#include // Vita3K emulator project // Copyright (C) 2025 Vita3K team // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. #include #include #include #include #include #include #include #include #include // Sockets #ifdef _WIN32 #include #else #include #include #include #include #include #include #endif #define LOG_GDB_LEVEL 0 #if LOG_GDB_LEVEL >= 1 #define LOG_GDB LOG_INFO #else #define LOG_GDB(a, ...) #endif #if LOG_GDB_LEVEL >= 2 #define LOG_GDB_DEBUG LOG_INFO #else #define LOG_GDB_DEBUG(a, ...) #endif // Credit to jfhs for their GDB stub for RPCS3 which this stub is based on. typedef char PacketData[1200]; struct PacketCommand { char *data{}; int64_t length = -1; int64_t begin_index = -1; int64_t end_index = -1; int64_t content_length = -1; char *content_start{}; char *checksum_start{}; uint8_t checksum = 0; bool is_valid = false; }; typedef std::function PacketFunction; struct PacketFunctionBundle { std::string_view name; PacketFunction function; }; static std::string content_string(PacketCommand &command) { return std::string(command.content_start, static_cast(command.content_length)); } // Hexes static std::string be_hex(uint32_t value) { return fmt::format("{:0>8x}", htonl(value)); } static std::string to_hex(uint32_t value) { return fmt::format("{:0>8x}", value); } static std::string to_hex(SceUID value) { return fmt::format("{:0>8x}", value); } static uint32_t parse_hex(const std::string &hex) { std::stringstream stream; uint32_t value; stream << std::hex << hex; stream >> value; return value; } static uint8_t make_checksum(const char *data, int64_t length) { size_t sum = 0; for (int64_t a = 0; a < length; a++) { sum += data[a]; } return static_cast(sum % 256); } static PacketCommand parse_command(char *data, int64_t length) { PacketCommand command = {}; command.data = data; command.length = length; // TODO: Use std::find() to find packet begin and end. if (length > 1) command.begin_index = 1; for (int64_t a = 0; a < length; a++) { if (data[a] == '#') command.end_index = a; } command.is_valid = command.begin_index != -1 && command.end_index != -1 && command.end_index > command.begin_index && command.end_index + 2 < length; if (!command.is_valid) return command; command.content_start = command.data + command.begin_index; command.content_length = command.end_index - command.begin_index; command.checksum_start = command.data + command.end_index + 1; command.checksum = static_cast(parse_hex(std::string(command.checksum_start, 2))); command.is_valid = make_checksum(command.content_start, command.content_length) == command.checksum; if (!command.is_valid) return command; return command; } static int64_t server_reply(GDBState &state, const char *data, int64_t length) { uint8_t checksum = make_checksum(data, length); std::string packet_data = fmt::format("${}#{:0>2x}", std::string(data, length), checksum); return send(state.client_socket, &packet_data[0], packet_data.size(), 0); } static int64_t server_reply(GDBState &state, const char *text) { return server_reply(state, text, strlen(text)); } static int64_t server_ack(GDBState &state, char ack = '+') { return send(state.client_socket, &ack, 1, 0); } static std::string cmd_supported(EmuEnvState &state, PacketCommand &command) { return "multiprocess-;swbreak+;hwbreak-;qRelocInsn-;fork-events-;vfork-events-;" "exec-events-;vContSupported+;QThreadEvents-;no-resumed-;xmlRegisters=arm"; } static std::string cmd_reply_empty(EmuEnvState &state, PacketCommand &command) { return ""; } // This function is not thread safe static SceUID select_thread(EmuEnvState &state, int thread_id) { if (thread_id == 0) { if (state.kernel.threads.empty()) return -1; return state.kernel.threads.begin()->first; } return thread_id; } static std::string cmd_set_current_thread(EmuEnvState &state, PacketCommand &command) { const auto guard = std::lock_guard(state.kernel.mutex); int32_t thread_id = parse_hex(std::string( command.content_start + 2, static_cast(command.content_length - 2))); switch (command.content_start[1]) { case 'c': LOG_GDB("GDB Server Deprecated Continue Option 'c'"); // state.gdb.current_continue_thread = select_thread(state, thread_id); break; case 'g': state.gdb.current_thread = select_thread(state, thread_id); break; default: LOG_GDB("GDB Server Unknown Set Current Thread OP. {}", command.content_start[1]); break; } return "OK"; } static std::string cmd_get_current_thread(EmuEnvState &state, PacketCommand &command) { return "QC" + to_hex(state.gdb.current_thread); } static uint32_t fetch_reg(CPUState &state, uint32_t reg) { if (reg <= 12) { return read_reg(state, reg); } if (reg == 13) return read_sp(state); if (reg == 14) return read_lr(state); if (reg == 15) return read_pc(state); if (reg <= 23) { float value = read_float_reg(state, reg - 16); return std::bit_cast(value); } if (reg == 24) return read_fpscr(state); if (reg == 25) return read_cpsr(state); LOG_GDB("GDB Server Queried Invalid Register {}", reg); return 0; } static void modify_reg(CPUState &state, uint32_t reg, uint32_t value) { if (reg <= 12) { write_reg(state, reg, value); return; } if (reg == 13) { write_sp(state, value); return; } if (reg == 14) { write_lr(state, value); return; } if (reg == 15) { write_pc(state, value); return; } if (reg <= 23) { write_float_reg(state, reg - 16, std::bit_cast(value)); return; } if (reg == 24) { write_fpscr(state, value); return; } if (reg == 25) { write_cpsr(state, value); return; } LOG_GDB("GDB Server Modified Invalid Register {}", reg); } static std::string cmd_read_registers(EmuEnvState &state, PacketCommand &command) { const auto guard = std::lock_guard(state.kernel.mutex); if (state.gdb.current_thread == -1 || !state.kernel.threads.contains(state.gdb.current_thread)) return "E00"; CPUState &cpu = *state.kernel.threads[state.gdb.current_thread]->cpu.get(); std::stringstream stream; for (uint32_t a = 0; a <= 15; a++) { stream << be_hex(fetch_reg(cpu, a)); } return stream.str(); } static std::string cmd_write_registers(EmuEnvState &state, PacketCommand &command) { const auto guard = std::lock_guard(state.kernel.mutex); if (state.gdb.current_thread == -1 || !state.kernel.threads.contains(state.gdb.current_thread)) return "E00"; CPUState &cpu = *state.kernel.threads[state.gdb.current_thread]->cpu.get(); const std::string content = content_string(command).substr(1); for (uint32_t a = 0; a < content.size() / 8; a++) { uint32_t value = parse_hex(content.substr(a * 8, 8)); modify_reg(cpu, a, value); } return "OK"; } static std::string cmd_read_register(EmuEnvState &state, PacketCommand &command) { const auto guard = std::lock_guard(state.kernel.mutex); if (state.gdb.current_thread == -1 || !state.kernel.threads.contains(state.gdb.current_thread)) return "E00"; CPUState &cpu = *state.kernel.threads[state.gdb.current_thread]->cpu.get(); const std::string content = content_string(command); uint32_t reg = parse_hex(content.substr(1, content.size() - 1)); return be_hex(fetch_reg(cpu, reg)); } static std::string cmd_write_register(EmuEnvState &state, PacketCommand &command) { const auto guard = std::lock_guard(state.kernel.mutex); if (state.gdb.current_thread == -1 || !state.kernel.threads.contains(state.gdb.current_thread)) return "E00"; CPUState &cpu = *state.kernel.threads[state.gdb.current_thread]->cpu.get(); const std::string content = content_string(command); size_t equal_index = content.find('='); uint32_t reg = parse_hex(content.substr(1, equal_index - 1)); uint32_t value = parse_hex(content.substr(equal_index + 1)); modify_reg(cpu, reg, value); return "OK"; } static bool check_memory_region(Address address, Address length, MemState &mem) { if (!address) { return false; } Address it = address; bool valid = true; for (; it < address + length; it += mem.page_size) { if (!is_valid_addr(mem, it)) { valid = false; break; } } return valid; } static std::string cmd_read_memory(EmuEnvState &state, PacketCommand &command) { const std::string content = content_string(command); const size_t pos = content.find(','); const std::string first = content.substr(1, pos - 1); const std::string second = content.substr(pos + 1); const uint32_t address = parse_hex(first); const uint32_t length = parse_hex(second); if (!check_memory_region(address, length, state.mem)) return "EAA"; std::stringstream stream; for (uint32_t a = 0; a < length; a++) { stream << fmt::format("{:0>2x}", *Ptr(address + a).get(state.mem)); } return stream.str(); } static std::string cmd_write_memory(EmuEnvState &state, PacketCommand &command) { const std::string content = content_string(command); const size_t pos_first = content.find(','); const size_t pos_second = content.find(':'); const std::string first = content.substr(1, pos_first - 1); const std::string second = content.substr(pos_first + 1, pos_second - pos_first); const uint32_t address = parse_hex(first); const uint32_t length = parse_hex(second); const std::string hex_data = content.substr(pos_second + 1); if (!check_memory_region(address, length, state.mem)) return "EAA"; for (uint32_t a = 0; a < length; a++) { *Ptr(address + a).get(state.mem) = static_cast(parse_hex(hex_data.substr(a * 2, 2))); } return "OK"; } // server_next() might not be able to tell the difference between the end of the packet ($) and 0x24 ($). // Thus, cmd_write_binary is disabled. static std::string cmd_write_binary(EmuEnvState &state, PacketCommand &command) { const std::string content = content_string(command); const size_t pos_first = content.find(','); const size_t pos_second = content.find(':'); const std::string first = content.substr(1, pos_first - 1); const std::string second = content.substr(pos_first + 1, pos_second - pos_first); const uint32_t address = parse_hex(first); const uint32_t length = parse_hex(second); const char *data = command.content_start + pos_second + 1; if (!check_memory_region(address, length, state.mem)) return "EAA"; for (uint32_t a = 0; a < length; a++) { *Ptr(address + a).get(state.mem) = data[a]; } return "OK"; } static std::string cmd_detach(EmuEnvState &state, PacketCommand &command) { return "OK"; } static std::string cmd_continue(EmuEnvState &state, PacketCommand &command) { const std::string content = content_string(command); constexpr auto watch_delay = std::chrono::milliseconds(100); uint64_t index = 5; uint64_t next = 0; do { next = content.find(';', index + 1); std::string text = content.substr(index + 1, next - index - 1); const char cmd = text[0]; switch (cmd) { case 'c': case 'C': case 's': case 'S': { bool step = cmd == 's' || cmd == 'S'; // inferior_thread is the thread that triggered breakpoint before // step or run that thread if (state.gdb.inferior_thread != 0) { const auto guard = std::lock_guard(state.kernel.mutex); auto thread = state.kernel.threads[state.gdb.inferior_thread]; auto thread_lock = std::unique_lock(thread->mutex); thread->resume(step); if (step) { // Wait until it finish stepping // TODO if that thread waits for sync primitive, dead lock. thread->status_cond.wait(thread_lock, [&]() { return thread->status == ThreadStatus::suspend; }); } } if (!step) { // resume the world { auto lock = std::unique_lock(state.kernel.mutex); for (const auto &pair : state.kernel.threads) { auto &thread = pair.second; if (thread->status == ThreadStatus::suspend) { lock.unlock(); thread->resume(); lock.lock(); thread->status_cond.wait(lock, [&]() { return thread->status != ThreadStatus::suspend; }); } } } // wait until some threads trigger breakpoint bool did_break = false; while (!did_break) { auto lock = std::unique_lock(state.kernel.mutex); if (state.gdb.server_die) return ""; for (const auto &[id, thread] : state.kernel.threads) { const auto thread_guard = std::lock_guard(thread->mutex); if (thread->status == ThreadStatus::suspend && hit_breakpoint(*thread->cpu)) { state.gdb.inferior_thread = id; did_break = true; break; } } lock.unlock(); std::this_thread::sleep_for(std::chrono::milliseconds(watch_delay)); } auto thread = state.kernel.get_thread(state.gdb.inferior_thread); LOG_INFO("GDB Breakpoint trigger (thread name: {}, thread_id: {})", thread->name, thread->id); LOG_INFO("PC: {} LR: {}", read_pc(*thread->cpu), read_lr(*thread->cpu)); LOG_INFO("{}", thread->log_stack_traceback()); // stop the world { auto lock = std::unique_lock(state.kernel.mutex); for (const auto &pair : state.kernel.threads) { auto thread = pair.second; if (thread->status == ThreadStatus::run) { thread->suspend(); thread->status_cond.wait(lock, [=]() { return thread->status == ThreadStatus::suspend || thread->status == ThreadStatus::dormant; }); } } } } state.gdb.current_thread = state.gdb.inferior_thread; return "S05"; } default: LOG_GDB("Unsupported vCont command '{}'", cmd); break; } index = next; } while (next != std::string::npos); return ""; } static std::string cmd_continue_supported(EmuEnvState &state, PacketCommand &command) { return "vCont;c;C;s;S;t;r"; } static std::string cmd_thread_alive(EmuEnvState &state, PacketCommand &command) { const auto guard = std::lock_guard(state.kernel.mutex); const std::string content = content_string(command); const int32_t thread_id = parse_hex(content.substr(1)); // Assuming a thread is removed from the map when it closes or is killed. if (state.kernel.threads.contains(thread_id)) return "OK"; return "E00"; } static std::string cmd_kill(EmuEnvState &state, PacketCommand &command) { return "OK"; } static std::string cmd_die(EmuEnvState &state, PacketCommand &command) { state.gdb.server_die = true; return ""; } static std::string cmd_attached(EmuEnvState &state, PacketCommand &command) { return "1"; } static std::string cmd_thread_status(EmuEnvState &state, PacketCommand &command) { return "T0"; } static std::string cmd_reason(EmuEnvState &state, PacketCommand &command) { return "S05"; } static std::string cmd_get_first_thread(EmuEnvState &state, PacketCommand &command) { const auto guard = std::lock_guard(state.kernel.mutex); std::stringstream stream; stream << "m"; stream << to_hex(state.kernel.threads.begin()->first); state.gdb.thread_info_index = 0; return stream.str(); } static std::string cmd_get_next_thread(EmuEnvState &state, PacketCommand &command) { const auto guard = std::lock_guard(state.kernel.mutex); std::stringstream stream; ++state.gdb.thread_info_index; if (state.gdb.thread_info_index == state.kernel.threads.size()) { stream << "l"; } else { auto iter = state.kernel.threads.begin(); std::advance(iter, state.gdb.thread_info_index); stream << "m"; stream << to_hex(iter->first); } return stream.str(); } static std::string cmd_add_breakpoint(EmuEnvState &state, PacketCommand &command) { const std::string content = content_string(command); const uint64_t first = content.find(','); const uint64_t second = content.find(',', first + 1); const uint32_t type = static_cast(std::stol(content.substr(1, first - 1))); const uint32_t address = parse_hex(content.substr(first + 1, second - 1 - first)); const uint32_t kind = static_cast(std::stol(content.substr(second + 1, content.size() - second - 1))); LOG_GDB("GDB Server New Breakpoint at {} ({}, {}).", log_hex(address), type, kind); // kind is 2 if it's thumb mode // https://sourceware.org/gdb/current/onlinedocs/gdb/ARM-Breakpoint-Kinds.html#ARM-Breakpoint-Kinds state.kernel.debugger.add_breakpoint(state.mem, address, kind == 2); return "OK"; } static std::string cmd_remove_breakpoint(EmuEnvState &state, PacketCommand &command) { const std::string content = content_string(command); const uint64_t first = content.find(','); const uint64_t second = content.find(',', first + 1); const uint32_t type = static_cast(std::stol(content.substr(1, first - 1))); const uint32_t address = parse_hex(content.substr(first + 1, second - 1 - first)); const uint32_t kind = static_cast(std::stol(content.substr(second + 1, content.size() - second - 1))); LOG_GDB("GDB Server Removed Breakpoint at {} ({}, {}).", log_hex(address), type, kind); state.kernel.debugger.remove_breakpoint(state.mem, address); return "OK"; } static std::string cmd_deprecated(EmuEnvState &state, PacketCommand &command) { LOG_GDB("GDB Server: Deprecated Packet. {}", content_string(command)); return ""; } static std::string cmd_unimplemented(EmuEnvState &state, PacketCommand &command) { LOG_GDB("GDB Server: Unimplemented Packet. {}", content_string(command)); return ""; } const static PacketFunctionBundle functions[] = { // General { "!", cmd_unimplemented }, { "?", cmd_reason }, { "H", cmd_set_current_thread }, { "T", cmd_thread_alive }, { "i", cmd_unimplemented }, { "I", cmd_unimplemented }, { "A", cmd_unimplemented }, { "bc", cmd_unimplemented }, { "bs", cmd_unimplemented }, { "t", cmd_unimplemented }, // Read/Write { "p", cmd_read_register }, { "P", cmd_write_register }, { "g", cmd_read_registers }, { "G", cmd_write_registers }, { "m", cmd_read_memory }, { "M", cmd_write_memory }, { "X", cmd_unimplemented }, // change cmd_unimplemented to cmd_write_binary to enable binary downloading // Query Packets { "qfThreadInfo", cmd_get_first_thread }, { "qsThreadInfo", cmd_get_next_thread }, { "qSupported", cmd_supported }, { "qAttached", cmd_attached }, { "qTStatus", cmd_thread_status }, { "qC", cmd_get_current_thread }, { "q", cmd_unimplemented }, { "Q", cmd_unimplemented }, // Shutdown { "d", cmd_unimplemented }, { "r", cmd_unimplemented }, { "R", cmd_unimplemented }, { "k", cmd_die }, // Control Packets { "vCont?", cmd_continue_supported }, { "vCont", cmd_continue }, { "vKill", cmd_kill }, { "vMustReplyEmpty", cmd_reply_empty }, { "v", cmd_unimplemented }, // Breakpoints { "z", cmd_remove_breakpoint }, { "Z", cmd_add_breakpoint }, // Deprecated { "b", cmd_deprecated }, { "B", cmd_deprecated }, { "c", cmd_deprecated }, { "C", cmd_deprecated }, { "s", cmd_deprecated }, { "S", cmd_deprecated }, }; template constexpr bool cmp_less(T t, U u) noexcept { using UT = std::make_unsigned_t; using UU = std::make_unsigned_t; if constexpr (std::is_signed_v == std::is_signed_v) return t < u; else if constexpr (std::is_signed_v) return t < 0 ? true : UT(t) < u; else return u < 0 ? false : t < UU(u); } static bool command_begins_with(PacketCommand &command, const std::string_view small_str) { // If the command's content is shorter than small_str, it can't match if (static_cast(command.content_length) < small_str.size()) return false; return std::memcmp(command.content_start, small_str.data(), small_str.size()) == 0; } static int64_t server_next(EmuEnvState &state) { PacketData buffer; // Wait for the server to close or a packet to be received. fd_set readSet; timeval timeout = { 1, 0 }; do { readSet = { 0 }; FD_SET(state.gdb.client_socket, &readSet); } while (select(state.gdb.client_socket + 1, &readSet, nullptr, nullptr, &timeout) < 1 && !state.gdb.server_die); if (state.gdb.server_die) return -1; const int64_t length = recv(state.gdb.client_socket, buffer, sizeof(buffer), 0); if (length <= 0) { LOG_GDB("GDB Server Connection Closed"); return -1; } buffer[length] = '\0'; for (int64_t a = 0; a < length; a++) { switch (buffer[a]) { case '+': { break; // Cool. } case '-': { LOG_GDB("GDB Server Transmission Error. {}", std::string(buffer, length)); server_reply(state.gdb, state.gdb.last_reply.c_str()); break; } case '$': { server_ack(state.gdb, '+'); PacketCommand command = parse_command(buffer + a, length - a); if (command.is_valid) { bool found_command = false; for (const auto &function : functions) { if (command_begins_with(command, function.name)) { found_command = true; LOG_GDB("GDB Server Recognized Command as {}. {}", function.name, std::string(command.content_start, command.content_length)); state.gdb.last_reply = function.function(state, command); if (state.gdb.server_die) break; server_reply(state.gdb, state.gdb.last_reply.c_str()); break; } } if (!found_command) { LOG_GDB("GDB Server Unrecognized Command. {}", std::string(command.content_start, command.content_length)); state.gdb.last_reply = ""; server_reply(state.gdb, state.gdb.last_reply.c_str()); } a += command.content_length + 3; } else { server_ack(state.gdb, '-'); LOG_GDB("GDB Server Invalid Command. {}", std::string(buffer, length)); } break; } default: break; } if (state.gdb.server_die) break; } return length; } static void server_listen(EmuEnvState &state) { state.gdb.client_socket = accept(state.gdb.listen_socket, nullptr, nullptr); if (state.gdb.client_socket == -1) { LOG_GDB("GDB Server Failed: Could not accept socket."); return; } LOG_INFO("GDB Server Received Connection"); int64_t status; do { status = server_next(state); } while (status >= 0 && !state.gdb.server_die); server_close(state); } void server_open(EmuEnvState &state) { LOG_GDB("Starting GDB Server..."); #ifdef _WIN32 int32_t err = WSAStartup(MAKEWORD(2, 2), &state.gdb.wsaData); if (err) { LOG_GDB("GDB Server Failed: Could not start WSA service."); return; } #endif state.gdb.listen_socket = socket(AF_INET, SOCK_STREAM, 0); if (state.gdb.listen_socket == -1) { LOG_GDB("GDB Server Failed: Could not create socket."); return; } sockaddr_in socket_address{}; socket_address.sin_family = AF_INET; socket_address.sin_port = htons(GDB_SERVER_PORT); #ifdef _WIN32 socket_address.sin_addr.S_un.S_addr = htonl(INADDR_ANY); #else socket_address.sin_addr.s_addr = htonl(INADDR_ANY); #endif if (bind(state.gdb.listen_socket, (sockaddr *)&socket_address, sizeof(socket_address)) == -1) { LOG_GDB("GDB Server Failed: Could not bind socket."); return; } if (listen(state.gdb.listen_socket, 1) == -1) { LOG_GDB("GDB Server Failed: Could not listen on socket."); return; } state.gdb.server_thread = std::make_shared(server_listen, std::ref(state)); LOG_INFO("GDB Server is listening on port {}", GDB_SERVER_PORT); } void server_close(EmuEnvState &state) { #ifdef _WIN32 closesocket(state.gdb.listen_socket); WSACleanup(); #else close(state.gdb.listen_socket); #endif state.gdb.server_die = true; if (state.gdb.server_thread && state.gdb.server_thread->get_id() != std::this_thread::get_id()) state.gdb.server_thread->join(); }