diff --git a/.vscode/launch.json b/.vscode/launch.json index ab489aa64e..bbdcdd830c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -31,5 +31,43 @@ "--fix" ] }, + // For use with the OpenGOAL VS Code extension. + // Before using either, start the REPL and the game, connect with (lt) and compile with (mi). + // The first one tries to resolve the game based on the active file, second one lets you pick game/port. + { + "name": "OpenGOAL: Attach to running game", + "type": "opengoal", + "request": "attach", + "projectRoot": "${workspaceFolder}" + }, + { + "name": "OpenGOAL: Attach to running game (choose game/port)", + "type": "opengoal", + "request": "attach", + "projectRoot": "${workspaceFolder}", + "port": "${input:opengoalDebugPort}" + }, + ], + "inputs": [ + { + "id": "opengoalDebugPort", + "type": "pickString", + "description": "Which game is running?", + "default": "8128", + "options": [ + { + "label": "Jak 1", + "value": "8128" + }, + { + "label": "Jak 2", + "value": "8129" + }, + { + "label": "Jak 3", + "value": "8130" + } + ] + } ] } \ No newline at end of file diff --git a/common/cross_os_debug/xdbg.cpp b/common/cross_os_debug/xdbg.cpp index 6ceadce4ae..69758605c8 100644 --- a/common/cross_os_debug/xdbg.cpp +++ b/common/cross_os_debug/xdbg.cpp @@ -301,6 +301,14 @@ bool cont_now(const ThreadID& tid) { return true; } +bool single_step_now(const ThreadID& tid) { + if (ptrace(PTRACE_SINGLESTEP, tid.id, nullptr, nullptr) < 0) { + printf("[Debugger] Failed to PTRACE_SINGLESTEP %s\n", strerror(errno)); + return false; + } + return true; +} + #elif _WIN32 ThreadID::ThreadID(DWORD _pid, DWORD _tid) : pid(_pid), tid(_tid) {} @@ -389,6 +397,41 @@ bool cont_now(const ThreadID& tid) { return true; } +/*! + * Execute a single instruction in the given thread, then stop again. + * Windows has no single-step request, so we set the x86 trap flag (EFLAGS.TF) and resume. The + * CPU raises a single-step exception after exactly one instruction. The flag is cleared by the + * CPU when the exception is delivered, so this does not need to be undone. + */ +bool single_step_now(const ThreadID& tid) { + CONTEXT context = {}; + context.ContextFlags = CONTEXT_CONTROL; + HANDLE hThr = OpenThread(THREAD_GET_CONTEXT | THREAD_SET_CONTEXT, FALSE, tid.tid); + + if (hThr == NULL) { + win_print_last_error("OpenThread single_step_now"); + return false; + } + + if (!GetThreadContext(hThr, &context)) { + win_print_last_error("GetThreadContext single_step_now"); + CloseHandle(hThr); + return false; + } + + context.EFlags |= 0x100; // TF + context.ContextFlags = CONTEXT_CONTROL; + + if (!SetThreadContext(hThr, &context)) { + win_print_last_error("SetThreadContext single_step_now"); + CloseHandle(hThr); + return false; + } + CloseHandle(hThr); + + return cont_now(tid); +} + DEBUG_EVENT debugEvent; void ignore_debug_event() { if (!ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE)) { @@ -570,7 +613,7 @@ bool write_goal_memory(const u8* src_buffer, const DebugContext& context, const MemoryHandle& mem) { SIZE_T written; - HANDLE hProc = OpenProcess(PROCESS_VM_WRITE, FALSE, context.tid.pid); + HANDLE hProc = OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION, FALSE, context.tid.pid); if (hProc == NULL) { win_print_last_error("OpenProcess write_goal_memory"); @@ -714,6 +757,9 @@ bool break_now(const ThreadID& tid) { bool cont_now(const ThreadID& tid) { return false; } +bool single_step_now(const ThreadID& tid) { + return false; +} bool open_memory(const ThreadID& tid, MemoryHandle* out) { return false; } diff --git a/common/cross_os_debug/xdbg.h b/common/cross_os_debug/xdbg.h index 2a01c4593b..e752b38a36 100644 --- a/common/cross_os_debug/xdbg.h +++ b/common/cross_os_debug/xdbg.h @@ -107,6 +107,7 @@ bool get_regs_now(const ThreadID& tid, Regs* out); bool set_regs_now(const ThreadID& tid, const Regs& in); bool break_now(const ThreadID& tid); bool cont_now(const ThreadID& tid); +bool single_step_now(const ThreadID& tid); bool open_memory(const ThreadID& tid, MemoryHandle* out); bool close_memory(const ThreadID& tid, MemoryHandle* handle); bool read_goal_memory(u8* dest_buffer, diff --git a/common/goos/TextDB.cpp b/common/goos/TextDB.cpp index 45f8f7b9e8..5c2895ca97 100644 --- a/common/goos/TextDB.cpp +++ b/common/goos/TextDB.cpp @@ -176,20 +176,23 @@ std::optional TextDb::get_short_info_for(const std::shared_pt } std::optional TextDb::try_get_short_info( - const std::shared_ptr& heap_obj) const { + const std::shared_ptr& heap_obj, + bool shorten_filename) const { auto it = m_map.find(heap_obj); if (it != m_map.end()) { auto& frag = it->second.frag; // shorten the string std::string name = frag->get_description(); - size_t start = 0; - for (size_t i = 0; i < name.size(); i++) { - if (name[i] == '/' || name[i] == '\\') { - start = i + 1; + if (shorten_filename) { + size_t start = 0; + for (size_t i = 0; i < name.size(); i++) { + if (name[i] == '/' || name[i] == '\\') { + start = i + 1; + } + } + if (start < name.size()) { + name = name.substr(start); } - } - if (start < name.size()) { - name = name.substr(start); } ShortInfo result; diff --git a/common/goos/TextDB.h b/common/goos/TextDB.h index c8775edd1a..d2e40536d7 100644 --- a/common/goos/TextDB.h +++ b/common/goos/TextDB.h @@ -112,7 +112,8 @@ class TextDb { std::optional get_short_info_for(const std::shared_ptr& frag, int offset) const; std::optional try_get_short_info(const Object& o) const; - std::optional try_get_short_info(const std::shared_ptr& o) const; + std::optional try_get_short_info(const std::shared_ptr& o, + bool shorten_filename = true) const; bool has_info(const Object& o) const; void inherit_info(const Object& parent, const Object& child); diff --git a/common/listener_common.h b/common/listener_common.h index 808382c7fe..ccd78ec3e7 100644 --- a/common/listener_common.h +++ b/common/listener_common.h @@ -60,4 +60,6 @@ struct ListenerMessageHeader { constexpr int DECI2_PORT = 8112; // TODO - is this a good choice? +constexpr int DEBUG_SERVER_PORT = 8128; + constexpr u16 DECI2_PROTOCOL = 0xe042; diff --git a/common/repl/config.cpp b/common/repl/config.cpp index 14aea63aa0..914d66ef68 100644 --- a/common/repl/config.cpp +++ b/common/repl/config.cpp @@ -8,6 +8,7 @@ namespace REPL { void to_json(json& j, const Config& obj) { j = json{ {"nreplPort", obj.nrepl_port}, + {"debugPort", obj.debug_port}, {"gameVersionFolder", obj.game_version_folder}, {"numConnectToTargetAttempts", obj.target_connect_attempts}, {"asmFileSearchDirs", obj.asm_file_search_dirs}, @@ -22,6 +23,9 @@ void from_json(const json& j, Config& obj) { if (j.contains("nreplPort")) { j.at("nreplPort").get_to(obj.nrepl_port); } + if (j.contains("debugPort")) { + j.at("debugPort").get_to(obj.debug_port); + } if (j.contains("gameVersionFolder")) { j.at("gameVersionFolder").get_to(obj.game_version_folder); } diff --git a/common/repl/config.h b/common/repl/config.h index 9684b2c07e..292cf5c1f3 100644 --- a/common/repl/config.h +++ b/common/repl/config.h @@ -5,6 +5,7 @@ #include #include +#include "common/listener_common.h" #include "common/versions/versions.h" #include "third-party/json.hpp" @@ -34,6 +35,8 @@ struct Config { // this is the default REPL configuration int nrepl_port = 8181; int temp_nrepl_port = -1; + int debug_port = -1; + int temp_debug_port = -1; std::string game_version_folder; int target_connect_attempts = 30; std::vector asm_file_search_dirs = {}; @@ -57,6 +60,16 @@ struct Config { } return nrepl_port; } + + int get_debug_port() { + if (temp_debug_port != -1) { + return temp_debug_port; + } + if (debug_port != -1) { + return debug_port; + } + return DEBUG_SERVER_PORT - 1 + (int)game_version; + } }; void to_json(json& j, const Config& obj); void from_json(const json& j, Config& obj); diff --git a/goalc/CMakeLists.txt b/goalc/CMakeLists.txt index 3796f14026..a9b02e7c49 100644 --- a/goalc/CMakeLists.txt +++ b/goalc/CMakeLists.txt @@ -71,6 +71,7 @@ add_library(compiler build_actor/jak3/build_actor.cpp debugger/Debugger.cpp debugger/DebugInfo.cpp + debugger/DebugServer.cpp emitter/IGenX86.cpp emitter/IGenARM64.cpp listener/Listener.cpp diff --git a/goalc/compiler/CodeGenerator.cpp b/goalc/compiler/CodeGenerator.cpp index cdb4260928..3866e884be 100644 --- a/goalc/compiler/CodeGenerator.cpp +++ b/goalc/compiler/CodeGenerator.cpp @@ -7,6 +7,7 @@ #include "CodeGenerator.h" +#include #include #include @@ -19,6 +20,103 @@ using namespace emitter; +namespace { + +void record_local_variables(const FunctionEnv* func, FunctionDebugInfo* debug) { + const auto& allocations = func->allocations(); + if (!allocations.ok || allocations.ass_as_ranges.empty()) { + return; + } + + // ireg id -> source name, parameters first, then anything bound in a lexical scope + std::unordered_map> names; + for (const auto& [symbol, reg_val] : func->params) { + if (reg_val) { + names[reg_val->ireg().id] = {symbol.name_ptr, true}; + } + } + for (const auto& env : func->child_envs()) { + auto* lexical = dynamic_cast(env.get()); + if (!lexical) { + continue; + } + for (const auto& [symbol, reg_val] : lexical->vars) { + if (reg_val && names.find(reg_val->ireg().id) == names.end()) { + names[reg_val->ireg().id] = {symbol.name_ptr, false}; + } + } + } + + if (names.empty()) { + return; + } + + const int instruction_count = int(func->code().size()); + + for (const auto& [ireg_id, name_info] : names) { + if (ireg_id < 0 || ireg_id >= int(allocations.ass_as_ranges.size()) || + ireg_id >= int(func->reg_vals().size())) { + continue; + } + const auto& range = allocations.ass_as_ranges.at(ireg_id); + + LocalVariableDebugInfo local; + local.name = name_info.first; + local.is_parameter = name_info.second; + local.type = func->reg_vals().at(ireg_id)->type(); + + for (int instr = 0; instr < instruction_count; instr++) { + if (!range.is_live_at_instr(instr)) { + continue; + } + const auto& assignment = range.get(instr); + if (!assignment.is_assigned()) { + continue; + } + + VariableLocation here; + here.start_ir = instr; + here.end_ir = instr; + if (assignment.kind == Assignment::Kind::REGISTER) { + here.kind = VariableLocation::Kind::REGISTER; + here.reg = assignment.reg.id(); + } else if (assignment.kind == Assignment::Kind::STACK) { + here.kind = VariableLocation::Kind::STACK; + // spilled variables sit at rsp + slot * 8, matching how the spill ops address them + here.stack_offset = allocations.get_slot_for_spill(assignment.stack_slot) * GPR_SIZE; + } else { + continue; + } + + // extend the previous interval instead of starting a new one where nothing changed + if (!local.locations.empty()) { + auto& previous = local.locations.back(); + if (previous.end_ir == instr - 1 && previous.kind == here.kind && + previous.reg == here.reg && previous.stack_offset == here.stack_offset) { + previous.end_ir = instr; + continue; + } + } + local.locations.push_back(here); + } + + if (!local.locations.empty()) { + debug->locals.push_back(std::move(local)); + } + } + + // parameters first, then alphabetically + std::sort(debug->locals.begin(), debug->locals.end(), + [](const LocalVariableDebugInfo& a, const LocalVariableDebugInfo& b) { + if (a.is_parameter != b.is_parameter) { + return a.is_parameter; + } + return a.name < b.name; + }); +} + +} // namespace + CodeGenerator::CodeGenerator(FileEnv* env, DebugInfo* debug_info, GameVersion version, @@ -48,6 +146,7 @@ std::vector CodeGenerator::run(const TypeSystem* ts) { for (auto& x : f->code()) { rec.debug->ir_strings.push_back(x->print()); } + record_local_variables(f.get(), rec.debug); } // next, add all static objects. diff --git a/goalc/compiler/Env.h b/goalc/compiler/Env.h index 9b928207f5..c26994e9dd 100644 --- a/goalc/compiler/Env.h +++ b/goalc/compiler/Env.h @@ -241,6 +241,10 @@ class FunctionEnv : public DeclareEnv { const std::vector>& reg_vals() const { return m_iregs; } + // all envs in a function, used by the debugger to get variable names + const std::vector>& child_envs() const { return m_envs; } + const AllocationResult& allocations() const { return m_regalloc_result; } + RegVal* push_reg_val(std::unique_ptr in); int segment = -1; diff --git a/goalc/debugger/DebugInfo.h b/goalc/debugger/DebugInfo.h index dc17255f43..fe48f4e8c0 100644 --- a/goalc/debugger/DebugInfo.h +++ b/goalc/debugger/DebugInfo.h @@ -7,6 +7,7 @@ #include #include "common/common_types.h" +#include "common/type_system/TypeSpec.h" #include "common/util/Assert.h" #include "goalc/debugger/disassemble.h" @@ -19,10 +20,33 @@ class Object; class HeapObject; } // namespace goos +// location of a variable over a range of IR instructions. +struct VariableLocation { + int start_ir = 0; + int end_ir = 0; + enum class Kind : u8 { REGISTER, STACK } kind = Kind::REGISTER; + int reg = -1; + int stack_offset = 0; +}; + +struct LocalVariableDebugInfo { + std::string name; + TypeSpec type; + bool is_parameter = false; + std::vector locations; + + const VariableLocation* location_at(int ir_idx) const { + for (const auto& loc : locations) { + if (ir_idx >= loc.start_ir && ir_idx <= loc.end_ir) { + return &loc; + } + } + return nullptr; + } +}; + /*! * FunctionDebugInfo stores per-function debugging information. - * For now, it is pretty basic, but it will eventually contain stuff like stack frame info - * and which var is in each register at each instruction. */ struct FunctionDebugInfo { u32 offset_in_seg; // not including type tag. @@ -40,6 +64,9 @@ struct FunctionDebugInfo { std::vector generated_code; std::optional stack_usage; + // named locals and parameters, for showing variable values at a breakpoint + std::vector locals; + std::string disassemble_debug_info(bool* had_failure, const goos::Reader* reader, bool omit_ir); }; @@ -72,6 +99,10 @@ class DebugInfo { FunctionDebugInfo& function_by_name(const std::string& name) { return m_functions.at(name); } + const std::unordered_map& functions() const { + return m_functions; + } + void clear() { m_functions.clear(); } std::string disassemble_all_functions(bool* had_failure, diff --git a/goalc/debugger/DebugServer.cpp b/goalc/debugger/DebugServer.cpp new file mode 100644 index 0000000000..df48e13342 --- /dev/null +++ b/goalc/debugger/DebugServer.cpp @@ -0,0 +1,1330 @@ +#include "DebugServer.h" + +#include +#include + +#include "common/cross_sockets/XSocket.h" +#include "common/goal_constants.h" +#include "common/log/log.h" +#include "common/util/math_util.h" +#include "common/versions/versions.h" + +#include "goalc/compiler/Compiler.h" +#include "goalc/emitter/Register.h" + +#include "fmt/format.h" + +namespace { + +struct GprName { + const char* name; + const char* label; + const char* role; + const char* group; +}; + +constexpr GprName GPR_NAMES[16] = { + {"rax", "v0", "return value (rax)", "general"}, + {"rcx", "arg3", "rcx", "arg"}, + {"rdx", "arg2", "rdx", "arg"}, + {"rbx", "rbx", "saved", "general"}, + {"rsp", "rsp", "stack pointer", "special"}, + {"rbp", "rbp", "saved", "general"}, + {"rsi", "arg1", "rsi", "arg"}, + {"rdi", "arg0", "rdi", "arg"}, + {"r8", "arg4", "r8", "arg"}, + {"r9", "arg5", "r9", "arg"}, + {"r10", "arg6", "r10", "arg"}, + {"r11", "arg7", "r11", "arg"}, + {"r12", "r12", "saved", "general"}, + {"r13", "pp", "current process (r13)", "special"}, + {"r14", "s7", "symbol table (r14)", "special"}, + {"r15", "ee", "EE memory base (r15)", "special"}, +}; + +constexpr int ARG_REGISTER_ORDER[8] = { + emitter::RDI, emitter::RSI, emitter::RDX, emitter::RCX, + emitter::R8, emitter::R9, emitter::R10, emitter::R11, +}; + +s64 sign_extend_from(u64 raw, int size) { + switch (size) { + case 1: + return (s8)raw; + case 2: + return (s16)raw; + case 4: + return (s32)raw; + default: + return (s64)raw; + } +} + +constexpr double METER_LENGTH = 4096.0; +constexpr double DEGREES_PER_ROT = 65536.0; +constexpr double TICKS_PER_SECOND = 300.0; + +std::string format_unit_number(double value) { + if (!std::isfinite(value)) { + return fmt::format("{}", value); + } + std::string result = fmt::format("{:.4f}", value); + if (result.find('.') != std::string::npos) { + result.erase(result.find_last_not_of('0') + 1); + if (!result.empty() && result.back() == '.') { + result.pop_back(); + } + } + return result; +} + +json float_special_types(double value) { + json out = json::array(); + out.push_back(fmt::format("(meters {})", format_unit_number(value / METER_LENGTH))); + out.push_back(fmt::format("(degrees {})", format_unit_number(value * 360.0 / DEGREES_PER_ROT))); + return out; +} + +json int_special_types(double value) { + json out = json::array(); + out.push_back(fmt::format("(seconds {})", format_unit_number(value / TICKS_PER_SECOND))); + return out; +} + +std::string format_enum_value(const EnumType* enum_type, u64 raw, int size) { + const std::string enum_name = enum_type->get_name(); + + if (enum_type->is_bitfield()) { + std::vector> set_bits; + u64 unaccounted = raw; + for (const auto& [name, bit] : enum_type->entries()) { + const u64 mask = (u64)1 << (u64)bit; + if (raw & mask) { + set_bits.emplace_back(bit, name); + unaccounted &= ~mask; + } + } + std::sort(set_bits.begin(), set_bits.end()); + + std::string form = "(" + enum_name; + for (const auto& [bit, name] : set_bits) { + (void)bit; + form += " " + name; + } + form += ")"; + + if (unaccounted) { + return fmt::format("{} 0x{:x} - unknown bits 0x{:x}", form, raw, unaccounted); + } + return fmt::format("{} 0x{:x}", form, raw); + } + + const s64 value = sign_extend_from(raw, size); + for (const auto& [name, entry_value] : enum_type->entries()) { + if (entry_value == value) { + return fmt::format("({} {}) {}", enum_name, name, value); + } + } + return fmt::format("{} - no matching entry in {}", value, enum_name); +} + +std::string format_symbol_name(const std::string& name) { + if (name == "#f" || name == "#t") { + return name; + } + return "'" + name; +} + +int element_stride(const Type* type, bool is_inline) { + if (!type) { + return 4; + } + if (is_inline && type->is_reference()) { + return align(type->get_size_in_memory(), type->get_inline_array_stride_alignment()); + } + return type->get_load_size(); +} + +std::string signal_kind_to_reason(xdbg::SignalInfo::Kind kind) { + switch (kind) { + case xdbg::SignalInfo::BREAK: + return "breakpoint"; + case xdbg::SignalInfo::SEGFAULT: + return "segfault"; + case xdbg::SignalInfo::MATH_EXCEPTION: + return "math exception"; + case xdbg::SignalInfo::ILLEGAL_INSTR: + return "illegal instruction"; + case xdbg::SignalInfo::DISAPPEARED: + return "exited"; + default: + return "unknown"; + } +} + +} // namespace + +DebugServer::~DebugServer() { + for (const int& sock : m_client_sockets) { + close_socket(sock); + } +} + +void DebugServer::post_init() { + lg::debug("[DebugServer:{}:{}] awaiting connections", tcp_port, listening_socket); +} + +void DebugServer::set_compiler(Compiler* compiler, std::mutex* compiler_mutex) { + m_compiler = compiler; + m_compiler_mutex = compiler_mutex; + m_stop_callback_installed = false; +} + +void DebugServer::install_stop_callback() { + if (m_stop_callback_installed || !m_compiler) { + return; + } + m_compiler->get_debugger().set_stop_callback([this](xdbg::SignalInfo::Kind kind) { + json body; + body["reason"] = signal_kind_to_reason(kind); + push_event(kind == xdbg::SignalInfo::DISAPPEARED ? "terminated" : "stopped", body); + }); + m_stop_callback_installed = true; +} + +void DebugServer::push_event(const std::string& event_name, const json& body) { + json event; + event["event"] = event_name; + event["body"] = body; + + std::lock_guard lock(m_event_mutex); + m_event_queue.push(event.dump()); +} + +void DebugServer::send_line(int socket, const std::string& line) { + const std::string payload = line + "\n"; + auto resp = write_to_socket(socket, payload.c_str(), (int)payload.size()); + if (resp == -1) { + lg::warn("[DebugServer:{}] client disconnected while writing", tcp_port); + close_socket(socket); + m_client_sockets.erase(socket); + } +} + +void DebugServer::accept_new_clients() { +#ifdef OS_POSIX + socklen_t addr_len = sizeof(addr); +#else + int addr_len = sizeof(addr); +#endif + auto new_socket = accept_socket(listening_socket, (sockaddr*)&addr, &addr_len); + if (new_socket < 0) { + return; + } + + if ((int)m_client_sockets.size() >= max_clients) { + lg::warn("[DebugServer:{}] maximum clients reached, rejecting connection", tcp_port); + close_socket(new_socket); + return; + } + + lg::info("[DebugServer:{}] new connection: {}", tcp_port, address_to_string(addr)); + m_client_sockets.insert(new_socket); + + // say hello, so the client can confirm it's talking to the right thing + json hello; + hello["event"] = "hello"; + hello["body"]["version"] = + fmt::format("{}.{}", versions::GOAL_VERSION_MAJOR, versions::GOAL_VERSION_MINOR); + send_line(new_socket, hello.dump()); +} + +void DebugServer::service_client(int socket) { + const int got = read_from_socket(socket, m_read_buffer.data(), (int)m_read_buffer.size()); + if (got == 0) { + lg::warn("[DebugServer:{}] client disconnected", tcp_port); + close_socket(socket); + m_client_sockets.erase(socket); + return; + } + if (got < 0) { + // nothing to read right now (the socket is non-blocking / timed out) + return; + } + + m_pending_input.append(m_read_buffer.data(), got); + + // requests are newline delimited, handle every complete one we have + size_t newline_pos; + while ((newline_pos = m_pending_input.find('\n')) != std::string::npos) { + const std::string line = m_pending_input.substr(0, newline_pos); + m_pending_input.erase(0, newline_pos + 1); + if (line.empty()) { + continue; + } + + json response; + auto parsed = safe_parse_json(line); + if (!parsed) { + response["ok"] = false; + response["error"] = "malformed json request"; + } else { + response = handle_request(*parsed); + } + send_line(socket, response.dump()); + + if (m_client_sockets.find(socket) == m_client_sockets.end()) { + // the write above disconnected us + return; + } + } +} + +void DebugServer::flush_events() { + std::queue to_send; + { + std::lock_guard lock(m_event_mutex); + std::swap(to_send, m_event_queue); + } + + while (!to_send.empty()) { + const std::string line = to_send.front(); + to_send.pop(); + // events go to everyone connected + for (auto it = m_client_sockets.begin(); it != m_client_sockets.end();) { + const int sock = *it; + ++it; + send_line(sock, line); + } + } +} + +void DebugServer::run_once() { + install_stop_callback(); + + // wait for activity on the listening socket or any client with a short timeout, so that queued + // events still go out promptly when nobody is sending us anything + fd_set read_sockets; + FD_ZERO(&read_sockets); + FD_SET(listening_socket, &read_sockets); + int max_sd = listening_socket; + for (const int& sock : m_client_sockets) { + if (sock > max_sd) { + max_sd = sock; + } + if (sock > 0) { + FD_SET(sock, &read_sockets); + } + } + + struct timeval timeout = {0, 50000}; + const auto activity = select(max_sd + 1, &read_sockets, nullptr, nullptr, &timeout); + if (activity < 0 && errno != EINTR) { + lg::error("[DebugServer:{}] select error: {}", tcp_port, strerror(errno)); + return; + } + + if (FD_ISSET(listening_socket, &read_sockets)) { + accept_new_clients(); + } + + // copy, because servicing a client can close it and mutate the set + const std::set clients = m_client_sockets; + for (const int sock : clients) { + if (FD_ISSET(sock, &read_sockets) && m_client_sockets.find(sock) != m_client_sockets.end()) { + service_client(sock); + } + } + + flush_events(); +} + +json DebugServer::handle_request(const json& request) { + json response; + if (request.contains("seq")) { + response["seq"] = request["seq"]; + } + + const std::string cmd = request.value("cmd", ""); + const json args = request.contains("args") ? request["args"] : json::object(); + + if (!m_compiler || !m_compiler_mutex) { + response["ok"] = false; + response["error"] = "compiler is not available"; + return response; + } + + try { + std::lock_guard lock(*m_compiler_mutex); + m_compiler->get_debugger().refresh_break_state(); + + json body; + if (cmd == "status") { + body = cmd_status(); + } else if (cmd == "attach") { + body = cmd_attach(); + } else if (cmd == "detach") { + body = cmd_detach(); + } else if (cmd == "pause") { + body = cmd_pause(); + } else if (cmd == "continue") { + body = cmd_continue(); + } else if (cmd == "step") { + body = cmd_step(args); + } else if (cmd == "set-breakpoints") { + body = cmd_set_breakpoints(args); + } else if (cmd == "stack") { + body = cmd_stack(); + } else if (cmd == "registers") { + body = cmd_registers(); + } else if (cmd == "read-memory") { + body = cmd_read_memory(args); + } else if (cmd == "evaluate") { + body = cmd_evaluate(args); + } else if (cmd == "inspect") { + body = cmd_inspect(args); + } else if (cmd == "locals") { + body = cmd_locals(); + } else { + response["ok"] = false; + response["error"] = fmt::format("unknown command '{}'", cmd); + return response; + } + + response["ok"] = true; + response["body"] = body; + } catch (const std::exception& e) { + response["ok"] = false; + response["error"] = e.what(); + } + + return response; +} + +json DebugServer::cmd_status() { + auto& dbg = m_compiler->get_debugger(); + json body; + body["valid"] = dbg.is_valid(); + body["attached"] = dbg.is_attached(); + body["halted"] = dbg.is_halted(); + body["running"] = dbg.is_running(); + return body; +} + +json DebugServer::cmd_attach() { + auto& dbg = m_compiler->get_debugger(); + if (!dbg.is_valid()) { + throw std::runtime_error("no valid debug context - is the game running and connected?"); + } + if (dbg.is_attached()) { + return cmd_status(); + } + if (!dbg.attach_and_break()) { + throw std::runtime_error("failed to attach to the target"); + } + return cmd_status(); +} + +json DebugServer::cmd_detach() { + auto& dbg = m_compiler->get_debugger(); + if (dbg.is_attached()) { + if (dbg.is_running()) { + dbg.set_suppress_stop_reporting(true); + dbg.do_break(); + } + + m_file_breakpoints.clear(); + clear_inspect_handles(); + dbg.detach(); + dbg.set_suppress_stop_reporting(false); + } + return cmd_status(); +} + +json DebugServer::cmd_pause() { + auto& dbg = m_compiler->get_debugger(); + if (!dbg.is_attached()) { + throw std::runtime_error("not attached"); + } + if (dbg.is_halted()) { + return cmd_status(); + } + if (!dbg.do_break()) { + throw std::runtime_error("failed to break"); + } + return cmd_status(); +} + +json DebugServer::cmd_continue() { + auto& dbg = m_compiler->get_debugger(); + if (!dbg.is_attached() || !dbg.is_halted()) { + throw std::runtime_error("not attached and halted"); + } + clear_inspect_handles(); + if (!dbg.resume_from_break()) { + throw std::runtime_error("failed to continue"); + } + push_event("continued", json::object()); + return cmd_status(); +} + +json DebugServer::cmd_step(const json& args) { + auto& dbg = m_compiler->get_debugger(); + if (!dbg.is_attached() || !dbg.is_halted()) { + throw std::runtime_error("not attached and halted"); + } + + const std::string kind_str = args.value("kind", "over"); + StepKind kind = StepKind::OVER; + if (kind_str == "in") { + kind = StepKind::INTO; + } else if (kind_str == "out") { + kind = StepKind::OUT_OF; + } + + clear_inspect_handles(); + if (!dbg.do_step(kind)) { + throw std::runtime_error(fmt::format("failed to step {}", kind_str)); + } + + // the watcher stays quiet during a step, so announce the stop ourselves + push_event("stopped", describe_stop("step")); + return cmd_status(); +} + +json DebugServer::cmd_set_breakpoints(const json& args) { + auto& dbg = m_compiler->get_debugger(); + const std::string file = args.value("file", ""); + if (file.empty()) { + throw std::runtime_error("set-breakpoints requires a file"); + } + + // we cannot set breakpoints while the game is running because we need to write to the memory, + // so stop first and temporarily suppress reporting stops + const bool was_running = dbg.is_attached() && dbg.is_running(); + if (was_running) { + dbg.set_suppress_stop_reporting(true); + dbg.do_break(); + } + + // DAP hands us the complete set for the file every time, so clear what we had + auto existing = m_file_breakpoints.find(file); + if (existing != m_file_breakpoints.end()) { + if (dbg.is_attached() && dbg.is_halted()) { + for (u32 addr : existing->second) { + dbg.remove_addr_breakpoint(addr); + } + } + m_file_breakpoints.erase(existing); + } + + json result = json::array(); + std::vector armed; + + if (args.contains("lines")) { + for (const auto& line_json : args["lines"]) { + const int line = line_json.get(); + json entry; + entry["line"] = line; + + auto resolved = dbg.resolve_source_breakpoint(file, line); + if (resolved.empty()) { + entry["verified"] = false; + entry["message"] = + "no compiled code on or after this line (has the file been compiled by goalc?)"; + result.push_back(entry); + continue; + } + + // a line can compile into more than one function (inlining, macros), so add all of them, + // but report the first back as the resolved location + bool armed_any = false; + bool any_loaded = false; + for (const auto& bp : resolved) { + if (!bp.loaded) { + continue; + } + any_loaded = true; + if (dbg.is_attached() && dbg.is_halted()) { + dbg.add_addr_breakpoint(bp.goal_addr); + armed.push_back(bp.goal_addr); + armed_any = true; + } + } + + entry["verified"] = armed_any; + entry["line"] = resolved.front().line; + entry["addr"] = resolved.front().goal_addr; + entry["function"] = resolved.front().function_name; + entry["object"] = resolved.front().object_name; + if (!armed_any) { + entry["message"] = + any_loaded + ? "resolved, but the target must be attached and halted to arm this breakpoint" + : fmt::format("resolved to {}, but object '{}' is not loaded in the target", + resolved.front().function_name, resolved.front().object_name); + } + result.push_back(entry); + } + } + + m_file_breakpoints[file] = armed; + + if (was_running) { + // put the target back the way we found it + if (dbg.is_halted()) { + dbg.resume_from_break(); + } + dbg.set_suppress_stop_reporting(false); + } + + json body; + body["breakpoints"] = result; + return body; +} + +json DebugServer::cmd_stack() { + auto& dbg = m_compiler->get_debugger(); + if (!dbg.is_attached() || !dbg.is_halted()) { + throw std::runtime_error("not attached and halted"); + } + + json frames = json::array(); + int id = 0; + for (const auto& frame : dbg.get_source_stack_frames()) { + json f; + f["id"] = id++; + f["name"] = frame.function_name; + f["object"] = frame.object_name; + f["addr"] = frame.goal_rip; + f["rsp"] = fmt::format("0x{:016x}", frame.rsp); + if (frame.source) { + f["file"] = frame.source->filename; + f["line"] = frame.source->line; + f["column"] = frame.source->column; + f["lineText"] = frame.source->line_text; + } + frames.push_back(f); + } + + json body; + body["frames"] = frames; + return body; +} + +json DebugServer::cmd_registers() { + auto& dbg = m_compiler->get_debugger(); + if (!dbg.is_attached() || !dbg.is_halted()) { + throw std::runtime_error("not attached and halted"); + } + if (!dbg.regs_valid()) { + throw std::runtime_error("register values are not available"); + } + + const auto& regs = dbg.get_regs(); + // 64 bit values go out as hex strings, but JSON numbers are doubles on the other end, which + // would silently round anything past 2^53 + auto describe_gpr = [&](int i) { + json r; + r["name"] = GPR_NAMES[i].name; + r["label"] = GPR_NAMES[i].label; + r["role"] = GPR_NAMES[i].role; + r["group"] = GPR_NAMES[i].group; + r["value"] = fmt::format("0x{:016x}", regs.gprs[i]); + + // GOAL pointer offset + const u32 goal_value = u32(regs.gprs[i]); + if (i == emitter::R13) { + r["goalValue"] = goal_value; + // try to get the type of the current process pointer and display it + auto type_name = runtime_type_of_basic(goal_value); + if (type_name) { + r["detail"] = *type_name; + } + } else if (i == emitter::R14 || i == emitter::R15) { + r["goalValue"] = goal_value; + } + return r; + }; + + json special = json::array(); + json args = json::array(); + json general = json::array(); + + for (int i = 0; i < 16; i++) { + const std::string group = GPR_NAMES[i].group; + if (group == "special") { + special.push_back(describe_gpr(i)); + } else if (group == "general") { + general.push_back(describe_gpr(i)); + } + } + // arguments go in arg order, not register order + for (int i = 0; i < 8; i++) { + args.push_back(describe_gpr(ARG_REGISTER_ORDER[i])); + } + + json gprs = json::array(); + for (int i = 0; i < 16; i++) { + gprs.push_back(describe_gpr(i)); + } + + json xmms = json::array(); + for (int i = 0; i < 16; i++) { + json r; + r["name"] = fmt::format("xmm{}", i); + float as_float[4]; + memcpy(as_float, ®s.xmms[i], sizeof(as_float)); + r["floats"] = {as_float[0], as_float[1], as_float[2], as_float[3]}; + u64 as_u64[2]; + memcpy(as_u64, ®s.xmms[i], sizeof(as_u64)); + r["lo"] = fmt::format("0x{:016x}", as_u64[0]); + r["hi"] = fmt::format("0x{:016x}", as_u64[1]); + xmms.push_back(r); + } + + json body; + body["special"] = special; + body["args"] = args; + body["general"] = general; + body["gprs"] = gprs; + body["xmms"] = xmms; + body["rip"] = fmt::format("0x{:016x}", regs.rip); + body["goalRip"] = u32(dbg.get_normalized_rip() - dbg.get_x86_base_addr()); + return body; +} + +json DebugServer::cmd_read_memory(const json& args) { + auto& dbg = m_compiler->get_debugger(); + if (!dbg.is_attached() || !dbg.is_halted()) { + throw std::runtime_error("not attached and halted"); + } + + const u32 addr = args.value("addr", 0u); + const int size = std::min(args.value("size", 16), 4096); + if (size <= 0) { + throw std::runtime_error("size must be positive"); + } + + std::vector buffer(size); + if (!dbg.read_memory_if_safe(buffer.data(), size, addr)) { + throw std::runtime_error(fmt::format("could not read {} bytes at 0x{:x}", size, addr)); + } + + std::string hex; + hex.reserve(size * 2); + for (u8 b : buffer) { + hex += fmt::format("{:02x}", b); + } + + json body; + body["addr"] = addr; + body["size"] = size; + body["data"] = hex; + return body; +} + +json DebugServer::cmd_evaluate(const json& args) { + auto& dbg = m_compiler->get_debugger(); + std::string expr = args.value("expr", ""); + // trim + while (!expr.empty() && std::isspace((unsigned char)expr.front())) { + expr.erase(expr.begin()); + } + while (!expr.empty() && std::isspace((unsigned char)expr.back())) { + expr.pop_back(); + } + if (expr.empty()) { + throw std::runtime_error("nothing to evaluate"); + } + + if (!dbg.is_attached() || !dbg.is_halted()) { + throw std::runtime_error("not attached and halted"); + } + + json body; + + const bool is_hex = expr.size() > 2 && expr[0] == '0' && (expr[1] == 'x' || expr[1] == 'X'); + const bool is_dec = + std::all_of(expr.begin(), expr.end(), [](char c) { return std::isdigit((unsigned char)c); }); + if (is_hex || is_dec) { + u32 addr = 0; + try { + addr = (u32)std::stoul(expr, nullptr, is_hex ? 16 : 10); + } catch (const std::exception&) { + throw std::runtime_error(fmt::format("could not parse '{}' as an address", expr)); + } + u32 word = 0; + if (!dbg.read_memory_if_safe(&word, addr)) { + throw std::runtime_error(fmt::format("could not read memory at 0x{:x}", addr)); + } + body["kind"] = "memory"; + body["addr"] = addr; + body["result"] = fmt::format("0x{:08x} ({})", word, (s32)word); + body["info"] = dbg.get_info_about_addr(addr); + if (auto type_name = runtime_type_of_basic(addr)) { + body["result"] = fmt::format("0x{:08x} ({})", addr, *type_name); + body["objectType"] = *type_name; + body["ref"] = make_inspect_handle({addr, *type_name, 0, false}); + } + return body; + } + + // try as symbol + const u32 sym_addr = dbg.get_symbol_address(expr); + if (!sym_addr) { + throw std::runtime_error(fmt::format("no symbol named '{}'", expr)); + } + u32 value = 0; + if (!dbg.get_symbol_value(expr, &value)) { + throw std::runtime_error(fmt::format("could not read the value of '{}'", expr)); + } + + body["kind"] = "symbol"; + body["addr"] = sym_addr; + body["value"] = value; + body["result"] = fmt::format("0x{:08x} ({})", value, (s32)value); + body["info"] = dbg.get_info_about_addr(value); + + // try to grab all the fields for basics + if (auto type_name = runtime_type_of_basic(value)) { + if (*type_name == "string") { + body["result"] = fmt::format("\"{}\"", read_goal_string(value)); + } else { + body["result"] = fmt::format("0x{:08x} ({})", value, *type_name); + } + body["objectType"] = *type_name; + body["ref"] = make_inspect_handle({value, *type_name, 0, false}); + } + return body; +} + +int DebugServer::make_inspect_handle(const InspectTarget& target) { + const int handle = m_next_inspect_handle++; + m_inspect_handles[handle] = target; + return handle; +} + +// clear inspect handles if we step forward since they may become invalid +void DebugServer::clear_inspect_handles() { + m_inspect_handles.clear(); +} + +std::string DebugServer::read_goal_string(u32 str_addr) { + auto& dbg = m_compiler->get_debugger(); + auto& types = m_compiler->type_system(); + + int data_offset = 8; + int tag_offset = BASIC_OFFSET; + auto* string_type = types.lookup_type_no_throw("string"); + if (string_type) { + tag_offset = string_type->get_offset(); + if (auto* structure = dynamic_cast(string_type)) { + for (const auto& field : structure->fields()) { + if (field.name() == "data") { + data_offset = field.offset(); + break; + } + } + } + } + + const u32 base_addr = str_addr - tag_offset; + + constexpr int kMaxChars = 128; + std::string result; + for (int i = 0; i < kMaxChars; i++) { + u8 c = 0; + if (!dbg.read_memory_if_safe(&c, base_addr + data_offset + i)) { + break; + } + if (c == 0) { + return result; + } + result.push_back((char)c); + } + return result + "..."; +} + +std::optional DebugServer::runtime_type_of_basic(u32 ptr) { + auto name = m_compiler->get_debugger().get_type_name_of_basic(ptr); + if (name && m_compiler->type_system().lookup_type_no_throw(*name)) { + return name; + } + return {}; +} + +std::optional DebugServer::read_word_field(const StructureType* structure, + u32 base_addr, + const std::string& name) { + auto& dbg = m_compiler->get_debugger(); + for (const auto& field : structure->fields()) { + if (field.name() == name && !field.is_dynamic() && !field.is_array()) { + s32 value = 0; + if (dbg.read_memory_if_safe(&value, base_addr + field.offset())) { + return value; + } + return {}; + } + } + return {}; +} + +// try to generate a field description with the given TypeSpec +json DebugServer::describe_field_value(u32 addr, const TypeSpec& type_spec, bool is_inline) { + auto& dbg = m_compiler->get_debugger(); + auto& types = m_compiler->type_system(); + + const std::string base = type_spec.base_type(); + auto* type = types.lookup_type_no_throw(base); + + if (is_inline) { + json out; + out["type"] = type_spec.print(); + const u32 tagged = addr + (type ? type->get_offset() : 0); + out["value"] = fmt::format("0x{:08x}", tagged); + out["inline"] = true; + out["ref"] = make_inspect_handle({tagged, base, 0, false}); + return out; + } + + u64 raw = 0; + if (type && !type->is_reference()) { + const int size = type->get_load_size(); + if (!dbg.read_memory_if_safe((u8*)&raw, std::min(size, 8), addr)) { + json out; + out["type"] = type_spec.print(); + out["value"] = ""; + return out; + } + } else { + u32 ptr = 0; + if (!dbg.read_memory_if_safe(&ptr, addr)) { + json out; + out["type"] = type_spec.print(); + out["value"] = ""; + return out; + } + raw = ptr; + } + + return describe_value_bits(raw, type_spec, addr); +} + +json DebugServer::describe_value_bits(u64 raw, const TypeSpec& type_spec, std::optional addr) { + auto& dbg = m_compiler->get_debugger(); + auto& types = m_compiler->type_system(); + + json out; + out["type"] = type_spec.print(); + + const std::string base = type_spec.base_type(); + auto* type = types.lookup_type_no_throw(base); + + if (type && !type->is_reference()) { + const int size = type->get_load_size(); + + if (auto* enum_type = types.try_enum_lookup(type_spec)) { + out["value"] = format_enum_value(enum_type, raw, size); + return out; + } + + auto* bitfield_type = dynamic_cast(type); + const bool is_duration = + types.tc(TypeSpec("uint64"), type_spec) || + (types.lookup_type_no_throw("time-frame") && types.tc(TypeSpec("time-frame"), type_spec)); + + // print #f instead of the address of s7 on handles, pointers and inline-arrays + if (raw == (u64)(u32)raw && (bitfield_type || base == "pointer" || base == "inline-array")) { + if (auto symbol_name = dbg.get_symbol_name_at_address((u32)raw)) { + out["value"] = format_symbol_name(*symbol_name); + return out; + } + } + + // print all fields of a bitfield type + if (bitfield_type && !bitfield_type->fields().empty()) { + out["value"] = fmt::format("0x{:x}", raw); + if (addr) { + out["ref"] = make_inspect_handle({*addr, base, 0, false}); + } + return out; + } + + if (base == "float") { + float as_float = 0; + memcpy(&as_float, &raw, sizeof(as_float)); + out["value"] = fmt::format("{}", as_float); + out["units"] = float_special_types(as_float); + } else if (types.tc(TypeSpec("uinteger"), type_spec)) { + out["value"] = fmt::format("{} (0x{:x})", raw, raw); + if (is_duration) { + out["units"] = int_special_types((double)raw); + } + } else { + const s64 as_signed = sign_extend_from(raw, size); + out["value"] = fmt::format("{} (0x{:x})", as_signed, raw); + if (is_duration) { + out["units"] = int_special_types((double)as_signed); + } + } + return out; + } + + // reference types + const u32 ptr = u32(raw); + if (ptr == 0) { + out["value"] = "0x0 (null)"; + return out; + } + + if (base == "symbol") { + auto name = dbg.get_symbol_name_at_address(ptr); + out["value"] = name ? format_symbol_name(*name) : fmt::format("0x{:08x}", ptr); + return out; + } + if (base == "type") { + auto name = dbg.get_symbol_name_for_value(ptr); + out["value"] = name ? *name : fmt::format("0x{:08x}", ptr); + return out; + } + + // if the value is inside the symbol table, print the symbol name + if (auto symbol_name = dbg.get_symbol_name_at_address(ptr)) { + out["value"] = format_symbol_name(*symbol_name); + return out; + } + + // resolve function names for function pointer fields + if (types.tc(TypeSpec("function"), type_spec)) { + std::string name; + if (auto symbol_name = dbg.get_symbol_name_for_value(ptr)) { + name = *symbol_name; + } else { + auto rip_info = dbg.get_rip_info(ptr + dbg.get_x86_base_addr()); + if (rip_info.knows_function) { + name = rip_info.function_name; + } + } + out["value"] = + name.empty() ? fmt::format("0x{:08x}", ptr) : fmt::format("0x{:08x} ({})", ptr, name); + if (!name.empty()) { + out["function"] = name; + } + return out; + } + + std::optional runtime_type; + if (types.tc(TypeSpec("basic"), type_spec)) { + runtime_type = runtime_type_of_basic(ptr); + } + const std::string expand_as = runtime_type.value_or(base); + + if (expand_as == "string") { + out["value"] = fmt::format("\"{}\"", read_goal_string(ptr)); + return out; + } + + // for overlay fields (e.g. root in process-drawable), only print the runtime type + if (runtime_type && *runtime_type != base) { + out["type"] = *runtime_type; + } + out["value"] = fmt::format("0x{:08x}", ptr); + out["ref"] = make_inspect_handle({ptr, expand_as, 0, false}); + return out; +} + +// for dynamic fields of special types like inline-array-class, try to determine the length of the +// array in order to be able to read and print all array entries +void DebugServer::describe_dynamic_field(json& entry, + const StructureType* structure, + const std::string& object_type, + u32 base_addr, + u32 field_addr, + const Field& field) { + auto& dbg = m_compiler->get_debugger(); + auto& types = m_compiler->type_system(); + + entry["type"] = field.type().print(); + entry["value"] = ""; + + std::string element_type = field.type().base_type(); + bool inline_elements = field.is_inline(); + + // boxed arrays + if (types.tc(TypeSpec("array"), TypeSpec(object_type))) { + if (auto content_type = read_word_field(structure, base_addr, "content-type")) { + if (auto name = dbg.get_symbol_name_for_value((u32)*content_type)) { + if (types.lookup_type_no_throw(*name)) { + element_type = *name; + inline_elements = false; + } + } + } + } + + if (!types.lookup_type_no_throw(element_type)) { + return; + } + + const auto length = read_word_field(structure, base_addr, "length"); + const auto allocated = read_word_field(structure, base_addr, "allocated-length"); + const auto count = length ? length : allocated; + if (!count || *count < 0) { + return; + } + + entry["type"] = fmt::format("{} [{}]", element_type, *count); + entry["value"] = (length && allocated && *length != *allocated) + ? fmt::format("0x{:08x} ({} of {} used)", field_addr, *length, *allocated) + : fmt::format("0x{:08x}", field_addr); + if (*count > 0) { + entry["ref"] = make_inspect_handle({field_addr, element_type, *count, inline_elements}); + } +} + +json DebugServer::inspect_target(const InspectTarget& target) { + auto& dbg = m_compiler->get_debugger(); + auto& types = m_compiler->type_system(); + + json body; + body["addr"] = target.addr; + body["type"] = target.type; + json fields = json::array(); + + if (target.array_count > 0) { + auto* element_type = types.lookup_type_no_throw(target.type); + const int stride = element_stride(element_type, target.inline_elements); + + constexpr int kMaxElements = 256; + const int count = std::min(target.array_count, kMaxElements); + for (int i = 0; i < count; i++) { + json entry = describe_field_value( + target.addr + i * stride, TypeSpec(target.type), + target.inline_elements && element_type && element_type->is_reference()); + entry["name"] = fmt::format("[{}]", i); + fields.push_back(entry); + } + if (count < target.array_count) { + json truncated; + truncated["name"] = "..."; + truncated["type"] = ""; + truncated["value"] = fmt::format("{} more elements not shown", target.array_count - count); + fields.push_back(truncated); + } + + body["fields"] = fields; + return body; + } + + auto* type = types.lookup_type_no_throw(target.type); + if (!type) { + throw std::runtime_error(fmt::format("unknown type '{}'", target.type)); + } + + // bitfield types + if (auto* bitfield_type = dynamic_cast(type)) { + u64 raw = 0; + const int load_size = bitfield_type->get_load_size(); + if (!dbg.read_memory_if_safe((u8*)&raw, std::min(load_size, 8), target.addr)) { + throw std::runtime_error(fmt::format("could not read memory at 0x{:x}", target.addr)); + } + body["summary"] = fmt::format("0x{:x}", raw); + + for (const auto& bit_field : bitfield_type->fields()) { + json entry; + entry["name"] = bit_field.name(); + entry["type"] = bit_field.type().print(); + entry["bitOffset"] = bit_field.offset(); + entry["bitSize"] = bit_field.size(); + + const u64 mask = bit_field.size() >= 64 ? ~(u64)0 : (((u64)1 << (u64)bit_field.size()) - 1); + const u64 extracted = (raw >> (u64)bit_field.offset()) & mask; + + if (auto* enum_type = types.try_enum_lookup(bit_field.type())) { + entry["value"] = format_enum_value(enum_type, extracted, (bit_field.size() + 7) / 8); + } else if (types.tc(TypeSpec("uinteger"), bit_field.type())) { + entry["value"] = fmt::format("{} (0x{:x})", extracted, extracted); + } else { + const int width = bit_field.size(); + s64 as_signed = (s64)extracted; + if (width < 64 && (extracted & ((u64)1 << (u64)(width - 1)))) { + as_signed = (s64)(extracted | ~mask); + } + entry["value"] = fmt::format("{} (0x{:x})", as_signed, extracted); + } + fields.push_back(entry); + } + + body["fields"] = fields; + return body; + } + + // value types + if (!type->is_reference()) { + json entry = describe_field_value(target.addr, TypeSpec(target.type), false); + entry["name"] = target.type; + fields.push_back(entry); + body["fields"] = fields; + return body; + } + + auto* structure = dynamic_cast(type); + if (!structure) { + throw std::runtime_error(fmt::format("'{}' has no fields to inspect", target.type)); + } + + if (target.type == "string") { + body["summary"] = fmt::format("\"{}\"", read_goal_string(target.addr)); + } + + // basic offset + const u32 base_addr = target.addr - type->get_offset(); + + // only display the last dynamic field (the others are inherited from parent types, we only care + // about the last one) + const Field* dynamic_field = nullptr; + for (const auto& field : structure->fields()) { + if (field.is_dynamic()) { + dynamic_field = &field; + } + } + + for (const auto& field : structure->fields()) { + if (field.is_dynamic() && &field != dynamic_field) { + continue; + } + + json entry; + entry["name"] = field.name(); + entry["offset"] = field.offset(); + + const u32 field_addr = base_addr + field.offset(); + + if (field.is_dynamic()) { + describe_dynamic_field(entry, structure, target.type, base_addr, field_addr, field); + fields.push_back(entry); + continue; + } + + if (field.is_array()) { + auto* element_type = types.lookup_type_no_throw(field.type().base_type()); + const int count = field.array_size(); + entry["type"] = fmt::format("{} [{}]", field.type().print(), count); + entry["value"] = fmt::format("0x{:08x}", field_addr); + if (element_type) { + entry["ref"] = + make_inspect_handle({field_addr, field.type().base_type(), count, field.is_inline()}); + } + fields.push_back(entry); + continue; + } + + json described = describe_field_value(field_addr, field.type(), field.is_inline()); + described["name"] = field.name(); + described["offset"] = field.offset(); + fields.push_back(described); + } + + body["fields"] = fields; + return body; +} + +json DebugServer::cmd_inspect(const json& args) { + auto& dbg = m_compiler->get_debugger(); + if (!dbg.is_attached() || !dbg.is_halted()) { + throw std::runtime_error("not attached and halted"); + } + + // either re-open something we handed out earlier... + if (args.contains("ref")) { + const int ref = args["ref"].get(); + auto kv = m_inspect_handles.find(ref); + if (kv == m_inspect_handles.end()) { + throw std::runtime_error("that value is from an earlier stop and is no longer available"); + } + return inspect_target(kv->second); + } + + // ...or start from an address + InspectTarget target; + target.addr = args.value("addr", 0u); + target.type = args.value("type", ""); + if (target.addr == 0) { + throw std::runtime_error("inspect needs an addr or a ref"); + } + + if (target.type.empty()) { + auto runtime_type = runtime_type_of_basic(target.addr); + if (!runtime_type) { + throw std::runtime_error(fmt::format( + "can't work out the type of the object at 0x{:x} - pass one explicitly", target.addr)); + } + target.type = *runtime_type; + } + + return inspect_target(target); +} + +json DebugServer::cmd_locals() { + auto& dbg = m_compiler->get_debugger(); + if (!dbg.is_attached() || !dbg.is_halted()) { + throw std::runtime_error("not attached and halted"); + } + if (!dbg.regs_valid()) { + throw std::runtime_error("register values are not available"); + } + + const auto& regs = dbg.get_regs(); + json variables = json::array(); + + for (const auto& var : dbg.get_live_variables()) { + const TypeSpec& type_spec = var.type; + + json entry; + if (var.in_register) { + u64 raw = 0; + if (var.reg >= emitter::XMM0 && var.reg <= emitter::XMM15) { + memcpy(&raw, ®s.xmms[var.reg - emitter::XMM0], sizeof(raw)); + } else if (var.reg >= 0 && var.reg < 16) { + raw = regs.gprs[var.reg]; + } else { + continue; + } + entry = describe_value_bits(raw, type_spec, {}); + entry["storage"] = var.reg >= emitter::XMM0 ? fmt::format("xmm{}", var.reg - emitter::XMM0) + : GPR_NAMES[var.reg].name; + } else { + entry = describe_field_value(var.stack_addr, type_spec, false); + entry["storage"] = fmt::format("stack 0x{:x}", var.stack_addr); + entry["addr"] = var.stack_addr; + } + + entry["name"] = var.name; + entry["parameter"] = var.is_parameter; + variables.push_back(entry); + } + + json body; + body["variables"] = variables; + return body; +} + +json DebugServer::describe_stop(const std::string& reason) { + auto& dbg = m_compiler->get_debugger(); + json body; + body["reason"] = reason; + + if (dbg.is_attached() && dbg.is_halted() && dbg.regs_valid()) { + const u32 goal_rip = u32(dbg.get_normalized_rip() - dbg.get_x86_base_addr()); + body["addr"] = goal_rip; + auto loc = dbg.get_source_location(goal_rip); + if (loc) { + body["file"] = loc->filename; + body["line"] = loc->line; + body["column"] = loc->column; + } + } + + return body; +} diff --git a/goalc/debugger/DebugServer.h b/goalc/debugger/DebugServer.h new file mode 100644 index 0000000000..f99ac47b77 --- /dev/null +++ b/goalc/debugger/DebugServer.h @@ -0,0 +1,102 @@ +#pragma once + +/*! + * @file DebugServer.h + * Front end for the OpenGOAL VS Code extension debugger. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/cross_sockets/XSocketServer.h" +#include "common/type_system/TypeSpec.h" +#include "common/util/json_util.h" + +class Compiler; +class Field; +class StructureType; + +class DebugServer : public XSocketServer { + public: + using XSocketServer::XSocketServer; + virtual ~DebugServer(); + + void post_init() override; + void set_compiler(Compiler* compiler, std::mutex* compiler_mutex); + void run_once(); + void push_event(const std::string& event_name, const json& body); + + private: + Compiler* m_compiler = nullptr; + std::mutex* m_compiler_mutex = nullptr; + + int max_clients = 4; + std::vector m_read_buffer = std::vector(64 * 1024); + std::set m_client_sockets = {}; + std::string m_pending_input; + + std::mutex m_event_mutex; + std::queue m_event_queue; + std::map> m_file_breakpoints; + + struct InspectTarget { + u32 addr = 0; + std::string type; + int array_count = 0; + bool inline_elements = false; + }; + std::map m_inspect_handles; + int m_next_inspect_handle = 1; + + int make_inspect_handle(const InspectTarget& target); + void clear_inspect_handles(); + + bool m_stop_callback_installed = false; + + void accept_new_clients(); + void service_client(int socket); + void flush_events(); + void send_line(int socket, const std::string& line); + void install_stop_callback(); + + json handle_request(const json& request); + + // command handlers + json cmd_status(); + json cmd_attach(); + json cmd_detach(); + json cmd_pause(); + json cmd_continue(); + json cmd_step(const json& args); + json cmd_set_breakpoints(const json& args); + json cmd_stack(); + json cmd_registers(); + json cmd_read_memory(const json& args); + json cmd_evaluate(const json& args); + json cmd_inspect(const json& args); + json cmd_locals(); + + json inspect_target(const InspectTarget& target); + json describe_field_value(u32 addr, const TypeSpec& type_spec, bool is_inline); + json describe_value_bits(u64 raw, const TypeSpec& type_spec, std::optional addr); + std::string read_goal_string(u32 str_addr); + std::optional runtime_type_of_basic(u32 ptr); + std::optional read_word_field(const StructureType* structure, + u32 base_addr, + const std::string& name); + void describe_dynamic_field(json& entry, + const StructureType* structure, + const std::string& object_type, + u32 base_addr, + u32 field_addr, + const Field& field); + json describe_stop(const std::string& reason); +}; diff --git a/goalc/debugger/Debugger.cpp b/goalc/debugger/Debugger.cpp index 2cfa62113d..b9a483be48 100644 --- a/goalc/debugger/Debugger.cpp +++ b/goalc/debugger/Debugger.cpp @@ -7,6 +7,7 @@ #include "Debugger.h" #include "common/goal_constants.h" +#include "common/goos/Reader.h" #include "common/log/log.h" #include "common/symbols.h" #include "common/util/Assert.h" @@ -63,6 +64,14 @@ bool Debugger::is_attached() const { bool Debugger::detach() { bool succ = true; if (is_valid() && m_attached) { + if (is_halted()) { + if (!m_regs_valid) { + m_regs_valid = xdbg::get_regs_now(m_debug_context.tid, &m_regs_at_break); + } + normalize_rip_after_break(); + remove_breakpoints(); + m_addr_breakpoints.clear(); + } #ifdef __linux__ if (!is_halted()) { succ = do_break(); @@ -226,11 +235,14 @@ InstructionPointerInfo Debugger::get_rip_info(u64 rip) { std::vector Debugger::get_backtrace(u64 rip, u64 rsp, - std::optional dump_path) { + std::optional dump_path, + bool quiet) { // TODO - it would probably be nice to decouple printing the backtrace from getting the backtrace // for now, build up a string and dump it at the end (if a path is provided) std::string backtrace_contents = ""; - lg::print("Backtrace:\n"); + if (!quiet) { + lg::print("Backtrace:\n"); + } std::vector bt; bool null_pc = rip == m_debug_context.base; @@ -239,7 +251,9 @@ std::vector Debugger::get_backtrace(u64 rip, u64 next_rip = 0; if (!read_memory_if_safe(&next_rip, rsp - m_debug_context.base)) { - lg::print("Failed to read return address off of the stack!\n"); + if (!quiet) { + lg::print("Failed to read return address off of the stack!\n"); + } return {}; } @@ -263,8 +277,10 @@ std::vector Debugger::get_backtrace(u64 rip, this_backtrace += fmt::format("{} from {}\n", frame.rip_info.function_name, frame.rip_info.func_debug->obj_name); // we're good! - auto disasm = disassemble_at_rip(frame.rip_info); - this_backtrace += fmt::format("{}\n", disasm.text); + if (!quiet) { + auto disasm = disassemble_at_rip(frame.rip_info); + this_backtrace += fmt::format("{}\n", disasm.text); + } u64 rsp_at_call = rsp + *frame.rip_info.func_debug->stack_usage; u64 next_rip = 0; @@ -353,7 +369,9 @@ std::vector Debugger::get_backtrace(u64 rip, backtrace_contents = this_backtrace + backtrace_contents; } - lg::print("{}\n", backtrace_contents); + if (!quiet) { + lg::print("{}\n", backtrace_contents); + } if (dump_path) { file_util::write_text_file(dump_path.value(), backtrace_contents); } @@ -439,9 +457,7 @@ Disassembly Debugger::disassemble_at_rip(const InstructionPointerInfo& info) { * Read the registers, symbol table, and instructions near rip. * Print out some info about where we are. */ -void Debugger::update_break_info(std::optional dump_path) { - // todo adjust rip if break instruction???? - +void Debugger::reload_break_state() { m_memory_map = m_listener->build_memory_map(); // lg::print("{}", m_memory_map.print()); read_symbol_table(); @@ -450,7 +466,24 @@ void Debugger::update_break_info(std::optional dump_path) { if (regs_valid()) { m_break_info = get_rip_info(m_regs_at_break.rip); update_continue_info(); + } +} +bool Debugger::refresh_break_state() { + if (!(is_valid() && is_attached() && is_halted())) { + return false; + } + if (m_regs_valid) { + return true; + } + reload_break_state(); + return m_regs_valid; +} + +void Debugger::update_break_info(std::optional dump_path) { + reload_break_state(); + + if (regs_valid()) { get_backtrace(m_regs_at_break.rip, m_regs_at_break.gprs[emitter::RSP], dump_path); auto dis = disassemble_at_rip(m_break_info); lg::print("{}\n", dis.text); @@ -493,25 +526,7 @@ bool Debugger::do_continue() { } ASSERT(regs_valid()); - if (!m_continue_info.valid) { - update_continue_info(); - } - ASSERT(m_continue_info.valid); - m_regs_valid = false; - - if (m_continue_info.subtract_1) { - m_regs_at_break.rip--; - auto result = xdbg::set_regs_now(m_debug_context.tid, m_regs_at_break); - ASSERT(result); - } - - m_expecting_immeidate_break = false; - if (!xdbg::cont_now(m_debug_context.tid)) { - return false; - } else { - m_running = true; - return true; - } + return resume_from_break(); } /*! @@ -950,13 +965,17 @@ void Debugger::watcher() { if (xdbg::check_stopped(m_debug_context.tid, &signal_info)) { // the target stopped! m_continue_info.valid = false; + const bool quiet = + m_suppress_stop_reporting.load() && signal_info.kind == xdbg::SignalInfo::BREAK; switch (signal_info.kind) { case xdbg::SignalInfo::SEGFAULT: printf("Target has crashed with a SEGFAULT! Run (:di) to get more information.\n"); break; case xdbg::SignalInfo::BREAK: - printf("Target has stopped. Run (:di) to get more information.\n"); + if (!quiet) { + printf("Target has stopped. Run (:di) to get more information.\n"); + } break; case xdbg::SignalInfo::MATH_EXCEPTION: printf("Target has crashed with a MATH_EXCEPTION! Run (:di) to get more information.\n"); @@ -994,6 +1013,11 @@ void Debugger::watcher() { } m_watcher_cv.notify_one(); + if (!quiet) { + // let the debug server (if available) tell its client we stopped + fire_stop_callback(signal_info.kind); + } + } else { // the target didn't stop. std::this_thread::sleep_for(std::chrono::milliseconds(10)); @@ -1055,6 +1079,11 @@ void Debugger::clear_signal_queue() { } void Debugger::add_addr_breakpoint(u32 addr) { + if (!is_halted()) { + lg::print("Cannot add a breakpoint unless the target is attached and halted.\n"); + return; + } + { std::unique_lock lock(m_watcher_mutex); auto kv = m_addr_breakpoints.find(addr); @@ -1083,6 +1112,11 @@ void Debugger::add_addr_breakpoint(u32 addr) { } void Debugger::remove_addr_breakpoint(u32 addr) { + if (!is_halted()) { + lg::print("Cannot remove a breakpoint unless the target is attached and halted.\n"); + return; + } + { std::unique_lock lock(m_watcher_mutex); update_continue_info(); @@ -1224,3 +1258,654 @@ std::string Debugger::disassemble_x86_with_symbols(int len, u64 base_addr) const return result; } + +namespace { +// compare the compiler's relative path with an absolute one from the editor +bool paths_match(const std::string& compiler_path, const std::string& query) { + auto normalize = [](const std::string& in) { + std::string out = in; + for (auto& c : out) { + if (c == '\\') { + c = '/'; + } +#ifdef _WIN32 + c = (char)std::tolower((unsigned char)c); +#endif + } + return out; + }; + + const std::string a = normalize(compiler_path); + const std::string b = normalize(query); + if (a == b) { + return true; + } + const std::string& longer = a.size() >= b.size() ? a : b; + const std::string& shorter = a.size() >= b.size() ? b : a; + if (shorter.empty() || longer.size() == shorter.size()) { + return false; + } + if (longer.compare(longer.size() - shorter.size(), shorter.size(), shorter) != 0) { + return false; + } + return longer[longer.size() - shorter.size() - 1] == '/'; +} +} // namespace + +void Debugger::fire_stop_callback(xdbg::SignalInfo::Kind kind) { + std::function cb; + { + std::lock_guard lock(m_stop_callback_mutex); + cb = m_stop_callback; + } + if (cb) { + cb(kind); + } +} + +u64 Debugger::get_normalized_rip() const { + const u64 rip = m_regs_at_break.rip; + if (!m_context_valid) { + return rip; + } + if (m_addr_breakpoints.find(u32(rip - m_debug_context.base - 1)) != m_addr_breakpoints.end()) { + return rip - 1; + } + return rip; +} + +std::optional Debugger::get_breakpoint_addr_at_stop() const { + if (!m_context_valid || !m_regs_valid) { + return {}; + } + const u32 addr = u32(m_regs_at_break.rip - m_debug_context.base - 1); + if (m_addr_breakpoints.find(addr) != m_addr_breakpoints.end()) { + return addr; + } + return {}; +} + +std::optional Debugger::source_location_for_function_offset( + const FunctionDebugInfo& func, + u32 function_offset) const { + if (!m_reader) { + return {}; + } + + // find the last instruction at or before this offset with src info + int best_ir = -1; + int best_offset = -1; + for (const auto& instr : func.instructions) { + if (instr.kind != InstructionInfo::Kind::IR || instr.offset < 0) { + continue; + } + if (u32(instr.offset) > function_offset) { + continue; + } + if (instr.offset >= best_offset) { + best_offset = instr.offset; + best_ir = instr.ir_idx; + } + } + + if (best_ir < 0 || best_ir >= int(func.code_sources.size())) { + return {}; + } + auto info = m_reader->db.try_get_short_info(func.code_sources.at(best_ir), false); + if (!info) { + return {}; + } + + SourceLocation loc; + loc.filename = info->filename; + loc.line = info->line_idx_to_display; + loc.column = info->pos_in_line; + loc.line_text = info->line_text; + return loc; +} + +std::optional Debugger::get_symbol_name_for_value(u32 value) const { + if (value == 0) { + return {}; + } + for (const auto& [name, sym_value] : m_symbol_name_to_value_map) { + if (sym_value == value) { + return name; + } + } + return {}; +} + +std::optional Debugger::get_symbol_name_at_address(u32 goal_addr) const { + if (!m_context_valid || goal_addr == 0) { + return {}; + } + + const s32 symbol_tag = m_version == GameVersion::Jak1 ? 0 : 1; + const s32 offset = s32(goal_addr) - s32(m_debug_context.s7) - symbol_tag; + + auto kv = m_symbol_offset_to_name_map.find(offset); + if (kv != m_symbol_offset_to_name_map.end()) { + return kv->second; + } + return {}; +} + +std::optional Debugger::get_type_name_of_basic(u32 goal_addr) { + if (!is_halted() || goal_addr < (u32)BASIC_OFFSET) { + return {}; + } + + u32 type_ptr = 0; + if (!read_memory_if_safe(&type_ptr, goal_addr - BASIC_OFFSET)) { + return {}; + } + return get_symbol_name_for_value(type_ptr); +} + +std::optional Debugger::get_source_location(u32 goal_addr) { + if (!m_context_valid) { + return {}; + } + auto info = get_rip_info(goal_addr + m_debug_context.base); + if (!info.knows_function || !info.func_debug) { + return {}; + } + return source_location_for_function_offset(*info.func_debug, info.function_offset); +} + +std::vector Debugger::resolve_source_breakpoint(const std::string& filename, + int line, + int max_line_slide) { + std::vector result; + if (!m_reader || !m_listener) { + return result; + } + + m_memory_map = m_listener->build_memory_map(); + + struct Candidate { + const FunctionDebugInfo* func = nullptr; + std::string func_name; + std::string obj_name; + int line = -1; + int offset = -1; + }; + std::vector candidates; + + for (auto& [obj_name, debug_info] : m_debug_info) { + for (const auto& [func_name, func] : debug_info.functions()) { + Candidate best; + for (const auto& instr : func.instructions) { + if (instr.kind != InstructionInfo::Kind::IR || instr.offset < 0) { + continue; + } + if (instr.ir_idx < 0 || instr.ir_idx >= int(func.code_sources.size())) { + continue; + } + auto info = m_reader->db.try_get_short_info(func.code_sources.at(instr.ir_idx), false); + if (!info || !paths_match(info->filename, filename)) { + continue; + } + const int instr_line = info->line_idx_to_display; + // a breakpoint on a blank line or a comment slides forward to the next line with code + if (instr_line < line || instr_line > line + max_line_slide) { + continue; + } + if (best.line == -1 || instr_line < best.line || + (instr_line == best.line && instr.offset < best.offset)) { + best.func = &func; + best.func_name = func_name; + best.obj_name = func.obj_name.empty() ? obj_name : func.obj_name; + best.line = instr_line; + best.offset = instr.offset; + } + } + if (best.func) { + candidates.push_back(best); + } + } + } + + if (candidates.empty()) { + return result; + } + + int chosen_line = candidates.front().line; + for (const auto& c : candidates) { + chosen_line = std::min(chosen_line, c.line); + } + + for (const auto& c : candidates) { + if (c.line != chosen_line) { + continue; + } + ResolvedBreakpoint bp; + bp.line = c.line; + bp.function_name = c.func_name; + bp.object_name = c.obj_name; + + listener::MemoryMapEntry entry; + if (m_memory_map.lookup(c.obj_name, c.func->seg, &entry)) { + bp.goal_addr = entry.start_addr + c.func->offset_in_seg + c.offset; + bp.loaded = true; + } + result.push_back(bp); + } + + return result; +} + +std::vector Debugger::get_live_variables() { + std::vector result; + if (!(is_valid() && is_attached() && is_halted()) || !m_regs_valid) { + return result; + } + + auto info = get_rip_info(get_normalized_rip()); + if (!info.knows_function || !info.func_debug || info.func_debug->locals.empty()) { + return result; + } + + int current_ir = -1; + int best_offset = -1; + for (const auto& instr : info.func_debug->instructions) { + if (instr.kind != InstructionInfo::Kind::IR || instr.offset < 0) { + continue; + } + if (u32(instr.offset) <= info.function_offset && instr.offset >= best_offset) { + best_offset = instr.offset; + current_ir = instr.ir_idx; + } + } + if (current_ir < 0) { + return result; + } + + const u64 rsp = m_regs_at_break.gprs[emitter::RSP]; + + for (const auto& local : info.func_debug->locals) { + const auto* location = local.location_at(current_ir); + if (!location) { + continue; + } + + LiveVariable var; + var.name = local.name; + var.type = local.type; + var.is_parameter = local.is_parameter; + + if (location->kind == VariableLocation::Kind::REGISTER) { + var.in_register = true; + var.reg = location->reg; + } else { + const u64 addr = rsp + location->stack_offset; + if (addr <= m_debug_context.base) { + continue; + } + var.stack_addr = u32(addr - m_debug_context.base); + } + + result.push_back(var); + } + + return result; +} + +std::vector Debugger::get_source_stack_frames(int max_frames) { + std::vector result; + if (!(is_valid() && is_attached() && is_halted()) || !m_regs_valid) { + return result; + } + + auto bt = get_backtrace(get_normalized_rip(), m_regs_at_break.gprs[emitter::RSP], {}, true); + + for (size_t i = 0; i < bt.size() && int(result.size()) < max_frames; i++) { + const auto& frame = bt.at(i); + SourceStackFrame out; + out.function_name = + frame.rip_info.knows_function ? frame.rip_info.function_name : "(unknown function)"; + out.object_name = frame.rip_info.knows_object ? frame.rip_info.object_name : ""; + out.rip = frame.rip_info.real_rip; + out.goal_rip = frame.rip_info.goal_rip; + out.rsp = frame.rsp_at_rip; + + if (frame.rip_info.func_debug) { + u32 offset = frame.rip_info.function_offset; + if (i > 0 && offset > 0) { + offset--; + } + out.source = source_location_for_function_offset(*frame.rip_info.func_debug, offset); + } + + result.push_back(out); + } + + return result; +} + +void Debugger::place_breakpoints() { + if (!is_halted()) { + return; + } + u8 int3 = 0xcc; + for (auto& [addr, bp] : m_addr_breakpoints) { + (void)bp; + write_memory(&int3, 1, addr); + } +} + +void Debugger::remove_breakpoints() { + if (!is_halted()) { + return; + } + for (auto& [addr, bp] : m_addr_breakpoints) { + write_memory(&bp.old_data, 1, addr); + } +} + +bool Debugger::normalize_rip_after_break() { + if (!m_regs_valid || !m_context_valid) { + return false; + } + + const u64 rip_goal = m_regs_at_break.rip - m_debug_context.base; + if (rip_goal == 0) { + return true; + } + if (m_addr_breakpoints.find(u32(rip_goal - 1)) == m_addr_breakpoints.end()) { + return true; + } + + m_regs_at_break.rip--; + if (!xdbg::set_regs_now(m_debug_context.tid, m_regs_at_break)) { + return false; + } + m_continue_info.valid = false; + update_continue_info(); + return true; +} + +bool Debugger::single_step_once() { + if (!(is_valid() && is_attached() && is_halted())) { + return false; + } + + m_continue_info.valid = false; + m_regs_valid = false; + clear_signal_queue(); + + if (!xdbg::single_step_now(m_debug_context.tid)) { + return false; + } + m_running = true; + + auto info = pop_signal(); + m_running = false; + if (info.kind == xdbg::SignalInfo::DISAPPEARED) { + return false; + } + + m_regs_valid = xdbg::get_regs_now(m_debug_context.tid, &m_regs_at_break); + return m_regs_valid; +} + +bool Debugger::resume_from_break() { + if (!(is_valid() && is_attached() && is_halted())) { + return false; + } + if (!m_regs_valid) { + m_regs_valid = xdbg::get_regs_now(m_debug_context.tid, &m_regs_at_break); + if (!m_regs_valid) { + return false; + } + } + const u64 rip_goal = m_regs_at_break.rip - m_debug_context.base; + auto bp_it = m_addr_breakpoints.find(u32(rip_goal)); + if (bp_it == m_addr_breakpoints.end() && rip_goal > 0) { + bp_it = m_addr_breakpoints.find(u32(rip_goal - 1)); + if (bp_it != m_addr_breakpoints.end()) { + m_regs_at_break.rip--; + if (!xdbg::set_regs_now(m_debug_context.tid, m_regs_at_break)) { + return false; + } + } + } + + if (bp_it != m_addr_breakpoints.end()) { + const auto bp = bp_it->second; + const bool was_suppressed = m_suppress_stop_reporting; + m_suppress_stop_reporting = true; + + bool ok = write_memory(&bp.old_data, 1, bp.goal_addr) && single_step_once(); + if (ok) { + u8 int3 = 0xcc; + ok = write_memory(&int3, 1, bp.goal_addr); + } + + m_suppress_stop_reporting = was_suppressed; + if (!ok) { + return false; + } + } + + m_continue_info.valid = false; + m_regs_valid = false; + m_expecting_immeidate_break = false; + clear_signal_queue(); + + if (!xdbg::cont_now(m_debug_context.tid)) { + return false; + } + m_running = true; + return true; +} + +bool Debugger::run_to_addr(u32 goal_addr) { + if (!is_halted()) { + return false; + } + + const bool already_a_user_bp = m_addr_breakpoints.find(goal_addr) != m_addr_breakpoints.end(); + u8 saved_byte = 0; + + if (!already_a_user_bp) { + if (!read_memory(&saved_byte, 1, goal_addr)) { + return false; + } + u8 int3 = 0xcc; + if (!write_memory(&int3, 1, goal_addr)) { + return false; + } + } + + bool arrived = false; + if (resume_from_break()) { + auto info = pop_signal(); + m_running = false; + if (info.kind != xdbg::SignalInfo::DISAPPEARED) { + m_regs_valid = xdbg::get_regs_now(m_debug_context.tid, &m_regs_at_break); + if (m_regs_valid) { + arrived = u32(m_regs_at_break.rip - m_debug_context.base - 1) == goal_addr; + } + } + } + + if (is_halted()) { + bool rewound = false; + if (!already_a_user_bp) { + // take our temporary int3 back out + write_memory(&saved_byte, 1, goal_addr); + if (arrived && m_regs_valid) { + // it was never in m_addr_breakpoints, so rewind off it by hand + m_regs_at_break.rip--; + xdbg::set_regs_now(m_debug_context.tid, m_regs_at_break); + rewound = true; + } + } + if (!rewound) { + // we may have stopped on one of the user's breakpoints instead of ours + normalize_rip_after_break(); + } + } + + m_continue_info.valid = false; + return arrived; +} + +std::optional Debugger::get_return_address_of_current_frame() { + if (!m_regs_valid) { + return {}; + } + auto info = get_rip_info(get_normalized_rip()); + if (!info.knows_function || !info.func_debug || !info.func_debug->stack_usage) { + return {}; + } + const u64 rsp_at_call = m_regs_at_break.gprs[emitter::RSP] + *info.func_debug->stack_usage; + u64 ret = 0; + if (!read_memory_if_safe(&ret, rsp_at_call - m_debug_context.base)) { + return {}; + } + return ret; +} + +bool Debugger::do_step(StepKind kind) { + if (!(is_valid() && is_attached() && is_halted())) { + return false; + } + if (!m_regs_valid) { + m_regs_valid = xdbg::get_regs_now(m_debug_context.tid, &m_regs_at_break); + if (!m_regs_valid) { + return false; + } + } + + m_memory_map = m_listener->build_memory_map(); + + const auto start_info = get_rip_info(get_normalized_rip()); + const FunctionDebugInfo* start_func = start_info.func_debug; + const u64 start_rsp = m_regs_at_break.gprs[emitter::RSP]; + int start_line = -1; + if (start_func) { + auto loc = source_location_for_function_offset(*start_func, start_info.function_offset); + if (loc) { + start_line = loc->line; + } + } + + m_suppress_stop_reporting = true; + bool ok = true; + + if (kind == StepKind::OUT_OF) { + auto ret = get_return_address_of_current_frame(); + if (ret && *ret > m_debug_context.base) { + ok = run_to_addr(u32(*ret - m_debug_context.base)); + } else { + ok = false; + } + } else { + if (!normalize_rip_after_break()) { + m_suppress_stop_reporting = false; + return false; + } + remove_breakpoints(); + + constexpr int MAX_STEPS = 500000; + int steps = 0; + + while (steps++ < MAX_STEPS) { + if (!single_step_once()) { + ok = false; + break; + } + + const u64 rip = m_regs_at_break.rip; + const u64 rsp = m_regs_at_break.gprs[emitter::RSP]; + auto info = get_rip_info(rip); + + const bool in_known_goal_code = info.in_goal_mem && info.knows_function && info.func_debug; + + if (!in_known_goal_code) { + // not in goal code, get return address from the top of the stack and get back to it + u64 ret = 0; + if (rsp > m_debug_context.base && + read_memory_if_safe(&ret, rsp - m_debug_context.base) && + ret > m_debug_context.base) { + place_breakpoints(); + const bool got_back = run_to_addr(u32(ret - m_debug_context.base)); + remove_breakpoints(); + if (!got_back) { + // we stopped for some other reason (breakpoint or crash), that stop wins + break; + } + continue; + } + // can't work out where we are or how to get back; stop here rather than run away + break; + } + + if (info.func_debug != start_func) { + if (rsp < start_rsp) { + // we've called into something + if (kind == StepKind::INTO) { + // settle on the first instruction in the callee with source info (the prologue + // usually doesn't have any) + int settle = 0; + while (settle++ < 200) { + auto here = get_rip_info(m_regs_at_break.rip); + if (here.func_debug && + source_location_for_function_offset(*here.func_debug, here.function_offset)) { + break; + } + if (!single_step_once()) { + ok = false; + break; + } + } + break; + } + + // stepping over, the return address is on top of the stack right after the call + u64 ret = 0; + if (rsp > m_debug_context.base && + read_memory_if_safe(&ret, rsp - m_debug_context.base) && + ret > m_debug_context.base) { + place_breakpoints(); + const bool got_back = run_to_addr(u32(ret - m_debug_context.base)); + remove_breakpoints(); + if (!got_back) { + break; + } + continue; + } + break; + } + + // we returned out of the function we started in, that's a completed step + break; + } + + // same function, are we on a new source line yet? + auto loc = source_location_for_function_offset(*info.func_debug, info.function_offset); + if (loc && loc->line != start_line) { + break; + } + } + + place_breakpoints(); + } + + m_suppress_stop_reporting = false; + + // refresh what everything else reads after a stop + if (ok && is_halted()) { + m_regs_valid = xdbg::get_regs_now(m_debug_context.tid, &m_regs_at_break); + if (m_regs_valid) { + m_break_info = get_rip_info(get_normalized_rip()); + } + m_continue_info.valid = false; + update_continue_info(); + } + + return ok; +} diff --git a/goalc/debugger/Debugger.h b/goalc/debugger/Debugger.h index 0915ef5c9f..f5461501ec 100644 --- a/goalc/debugger/Debugger.h +++ b/goalc/debugger/Debugger.h @@ -6,9 +6,12 @@ #pragma once +#include #include #include +#include #include +#include #include #include #include @@ -60,6 +63,42 @@ struct BacktraceFrame { u64 rsp_at_rip = 0; }; +struct SourceLocation { + std::string filename; + int line = -1; + int column = -1; + std::string line_text; +}; + +struct ResolvedBreakpoint { + u32 goal_addr = 0; + int line = -1; + std::string function_name; + std::string object_name; + bool loaded = false; +}; + +struct SourceStackFrame { + std::string function_name; + std::string object_name; + u64 rip = 0; + u32 goal_rip = 0; + u64 rsp = 0; + std::optional source; +}; + +struct LiveVariable { + std::string name; + TypeSpec type; + bool is_parameter = false; + + bool in_register = false; + int reg = -1; //! emitter::Register id, when in_register + u32 stack_addr = 0; //! GOAL address of the value, when spilled to the stack +}; + +enum class StepKind { OVER, INTO, OUT_OF }; + class Debugger { public: explicit Debugger(listener::Listener* listener, const goos::Reader* reader, GameVersion version) @@ -98,6 +137,7 @@ class Debugger { void add_addr_breakpoint(u32 addr); void remove_addr_breakpoint(u32 addr); void update_break_info(std::optional dump_path); + bool refresh_break_state(); InstructionPointerInfo get_rip_info(u64 x86_rip); DebugInfo& get_debug_info_for_object(const std::string& object_name); @@ -106,7 +146,35 @@ class Debugger { std::string get_info_about_addr(u32 addr); Disassembly disassemble_at_rip(const InstructionPointerInfo& info); - std::vector get_backtrace(u64 rip, u64 rsp, std::optional dump_path); + std::vector get_backtrace(u64 rip, + u64 rsp, + std::optional dump_path, + bool quiet = false); + std::optional get_source_location(u32 goal_addr); + std::optional get_type_name_of_basic(u32 goal_addr); + std::optional get_symbol_name_for_value(u32 value) const; + std::optional get_symbol_name_at_address(u32 goal_addr) const; + + std::vector resolve_source_breakpoint(const std::string& filename, + int line, + int max_line_slide = 50); + + std::vector get_source_stack_frames(int max_frames = 128); + std::vector get_live_variables(); + + bool resume_from_break(); + + bool do_step(StepKind kind); + + void set_stop_callback(std::function cb) { + std::lock_guard lock(m_stop_callback_mutex); + m_stop_callback = std::move(cb); + } + + u64 get_normalized_rip() const; + std::optional get_breakpoint_addr_at_stop() const; + + void set_suppress_stop_reporting(bool suppress) { m_suppress_stop_reporting = suppress; } std::string disassemble_x86_with_symbols(int len, u64 base_addr) const; @@ -178,6 +246,7 @@ class Debugger { void start_watcher(); void stop_watcher(); void watcher(); + void reload_break_state(); void update_continue_info(); void handle_disappearance(); @@ -219,6 +288,20 @@ class Debugger { bool m_running = true; bool m_attached = false; + std::atomic_bool m_suppress_stop_reporting{false}; + + std::mutex m_stop_callback_mutex; + std::function m_stop_callback; + void fire_stop_callback(xdbg::SignalInfo::Kind kind); + bool normalize_rip_after_break(); + bool single_step_once(); + bool run_to_addr(u32 goal_addr); + std::optional get_return_address_of_current_frame(); + void place_breakpoints(); + void remove_breakpoints(); + std::optional source_location_for_function_offset(const FunctionDebugInfo& func, + u32 function_offset) const; + InstructionPointerInfo m_break_info; listener::Listener* m_listener = nullptr; diff --git a/goalc/main.cpp b/goalc/main.cpp index 0c50d802a7..23906752a3 100644 --- a/goalc/main.cpp +++ b/goalc/main.cpp @@ -13,6 +13,7 @@ #include "common/versions/versions.h" #include "goalc/compiler/Compiler.h" +#include "goalc/debugger/DebugServer.h" #include "fmt/color.h" #include "fmt/format.h" @@ -37,6 +38,7 @@ int main(int argc, char** argv) { std::string username = "#f"; std::string game = "jak1"; int nrepl_port = -1; + int debug_port = -1; fs::path project_path_override; fs::path iso_path_override; @@ -46,6 +48,10 @@ int main(int argc, char** argv) { app.add_option("-u,--user", username, "Specify the username to use for your user profile in 'goal_src/user/'"); app.add_option("-p,--port", nrepl_port, "Specify the nREPL port. Defaults to 8181"); + app.add_option("--debug-port", debug_port, + "Specify the port for the JSON debug server that external debuggers (such as the " + "VS Code extension) connect to. Defaults to 8128 for jak1, 8129 for jak2, 8130 " + "for jak3"); app.add_flag("--user-auto", auto_find_user, "Attempt to automatically deduce the user, overrides '--user'"); app.add_option("-g,--game", game, "The game name: 'jak1' or 'jak2'"); @@ -86,6 +92,7 @@ int main(int argc, char** argv) { auto startup_file = REPL::load_user_startup_file(username, game_version); // Load the user's REPL config auto repl_config = REPL::load_repl_config(username, game_version, nrepl_port); + repl_config.temp_debug_port = debug_port; // Check for a custom ISO path before we instantiate the compiler. if (!iso_path_override.empty()) { @@ -127,11 +134,27 @@ int main(int argc, char** argv) { ReplServer repl_server(shutdown_callback, repl_config.get_nrepl_port()); bool nrepl_server_ok = repl_server.init_server(true); std::thread nrepl_thread; + + // Initialize the debug server socket for external debuggers to connect to + DebugServer debug_server(shutdown_callback, repl_config.get_debug_port()); + bool debug_server_ok = debug_server.init_server(true); + std::thread debug_thread; + // the compiler may throw an exception if it fails to load its standard library. try { compiler = std::make_unique( game_version, emitter::InstructionSet::X86, std::make_optional(repl_config), username, std::make_unique(username, repl_config, startup_file, nrepl_server_ok)); + + if (debug_server_ok) { + debug_server.set_compiler(compiler.get(), &compiler_mutex); + debug_thread = std::thread([&]() { + while (!shutdown_callback()) { + debug_server.run_once(); + } + }); + } + // Start nREPL Server if it spun up successfully if (nrepl_server_ok) { nrepl_thread = std::thread([&]() { @@ -160,6 +183,9 @@ int main(int argc, char** argv) { compiler = std::make_unique( game_version, emitter::InstructionSet::X86, std::make_optional(repl_config), username, std::make_unique(username, repl_config, startup_file, nrepl_server_ok)); + if (debug_server_ok) { + debug_server.set_compiler(compiler.get(), &compiler_mutex); + } status = ReplStatus::OK; } // process user input @@ -182,5 +208,9 @@ int main(int argc, char** argv) { repl_server.shutdown_server(); nrepl_thread.join(); } + if (debug_server_ok) { + debug_server.shutdown_server(); + debug_thread.join(); + } return 0; }