mirror of
https://github.com/open-goal/jak-project
synced 2026-08-06 09:54:10 -04:00
goalc: add debug server (#4365)
This adds a debug server to `goalc` that sends JSON over the socket to communicate with an external debugger using the Debug Adapter Protocol. This lets us debug GOAL code in a proper debugger with breakpoints, step over, step in and step out per line, stack frames and supports watches for global symbols, registers and local variables (local variables only work within the most recent stack frame). Special registers (`r13`, `r14`, `r15`, argument registers, etc.) are tracked separately and the current process register even displays the type of the current `pp` if possible. Watches that track addresses holding a reference type generate a list of field names according to the object's type. All fields will show their name, type and value and, depending on the type, will try to infer extra info like symbol names/values, function names for `function` fields, enum values and more. This also works nested, so any field that is also a reference type can also be accessed and display its fields, etc. Dynamic arrays are also supported where possible, e.g. in `inline-array-class` children and boxed arrays, it will figure out the value of the `length` field and access the memory up to that point so all the elements can be accessed and viewed from the `data` field. Our VS Code extension implements the DAP in open-goal/opengoal-vscode#375. Using it is as simple as connecting a REPL to a running game instance with `(lt)`, compiling with `(mi)` and, with the extension installed, pressing F5 in VS Code to start the debugger. By default, it will try to connect to the game that the active `.gc` file is from, the socket port is different per game (8128 for Jak 1, 8129 for Jak 2, 8130 for Jak 3). The `launch.json` was updated with two entries for this, the second entry lets you pick the game/port manually if desired. ~~Not tested on Windows.~~ Only supports x86 for now.
This commit is contained in:
Vendored
+38
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
+11
-8
@@ -176,20 +176,23 @@ std::optional<TextDb::ShortInfo> TextDb::get_short_info_for(const std::shared_pt
|
||||
}
|
||||
|
||||
std::optional<TextDb::ShortInfo> TextDb::try_get_short_info(
|
||||
const std::shared_ptr<goos::HeapObject>& heap_obj) const {
|
||||
const std::shared_ptr<goos::HeapObject>& 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;
|
||||
|
||||
@@ -112,7 +112,8 @@ class TextDb {
|
||||
std::optional<ShortInfo> get_short_info_for(const std::shared_ptr<SourceText>& frag,
|
||||
int offset) const;
|
||||
std::optional<ShortInfo> try_get_short_info(const Object& o) const;
|
||||
std::optional<ShortInfo> try_get_short_info(const std::shared_ptr<goos::HeapObject>& o) const;
|
||||
std::optional<ShortInfo> try_get_short_info(const std::shared_ptr<goos::HeapObject>& o,
|
||||
bool shorten_filename = true) const;
|
||||
|
||||
bool has_info(const Object& o) const;
|
||||
void inherit_info(const Object& parent, const Object& child);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#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<std::string> 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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "CodeGenerator.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <stdexcept>
|
||||
#include <unordered_set>
|
||||
|
||||
@@ -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<int, std::pair<std::string, bool>> 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<LexicalEnv*>(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<u8> 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.
|
||||
|
||||
@@ -241,6 +241,10 @@ class FunctionEnv : public DeclareEnv {
|
||||
|
||||
const std::vector<std::unique_ptr<RegVal>>& reg_vals() const { return m_iregs; }
|
||||
|
||||
// all envs in a function, used by the debugger to get variable names
|
||||
const std::vector<std::unique_ptr<Env>>& child_envs() const { return m_envs; }
|
||||
const AllocationResult& allocations() const { return m_regalloc_result; }
|
||||
|
||||
RegVal* push_reg_val(std::unique_ptr<RegVal> in);
|
||||
|
||||
int segment = -1;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <vector>
|
||||
|
||||
#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<VariableLocation> 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<u8> generated_code;
|
||||
std::optional<int> stack_usage;
|
||||
|
||||
// named locals and parameters, for showing variable values at a breakpoint
|
||||
std::vector<LocalVariableDebugInfo> 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<std::string, FunctionDebugInfo>& functions() const {
|
||||
return m_functions;
|
||||
}
|
||||
|
||||
void clear() { m_functions.clear(); }
|
||||
|
||||
std::string disassemble_all_functions(bool* had_failure,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,102 @@
|
||||
#pragma once
|
||||
|
||||
/*!
|
||||
* @file DebugServer.h
|
||||
* Front end for the OpenGOAL VS Code extension debugger.
|
||||
*/
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <queue>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#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<char> m_read_buffer = std::vector<char>(64 * 1024);
|
||||
std::set<int> m_client_sockets = {};
|
||||
std::string m_pending_input;
|
||||
|
||||
std::mutex m_event_mutex;
|
||||
std::queue<std::string> m_event_queue;
|
||||
std::map<std::string, std::vector<u32>> m_file_breakpoints;
|
||||
|
||||
struct InspectTarget {
|
||||
u32 addr = 0;
|
||||
std::string type;
|
||||
int array_count = 0;
|
||||
bool inline_elements = false;
|
||||
};
|
||||
std::map<int, InspectTarget> 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<u32> addr);
|
||||
std::string read_goal_string(u32 str_addr);
|
||||
std::optional<std::string> runtime_type_of_basic(u32 ptr);
|
||||
std::optional<s32> 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);
|
||||
};
|
||||
+714
-29
@@ -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<BacktraceFrame> Debugger::get_backtrace(u64 rip,
|
||||
u64 rsp,
|
||||
std::optional<std::string> dump_path) {
|
||||
std::optional<std::string> 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<BacktraceFrame> bt;
|
||||
|
||||
bool null_pc = rip == m_debug_context.base;
|
||||
@@ -239,7 +251,9 @@ std::vector<BacktraceFrame> Debugger::get_backtrace(u64 rip,
|
||||
|
||||
u64 next_rip = 0;
|
||||
if (!read_memory_if_safe<u64>(&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<BacktraceFrame> 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<BacktraceFrame> 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<std::string> 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<std::string> 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<std::string> 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<std::mutex> 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<std::mutex> 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<void(xdbg::SignalInfo::Kind)> cb;
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<u32> 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<SourceLocation> 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<std::string> 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<std::string> 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<std::string> 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<u32>(&type_ptr, goal_addr - BASIC_OFFSET)) {
|
||||
return {};
|
||||
}
|
||||
return get_symbol_name_for_value(type_ptr);
|
||||
}
|
||||
|
||||
std::optional<SourceLocation> 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<ResolvedBreakpoint> Debugger::resolve_source_breakpoint(const std::string& filename,
|
||||
int line,
|
||||
int max_line_slide) {
|
||||
std::vector<ResolvedBreakpoint> 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<Candidate> 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<LiveVariable> Debugger::get_live_variables() {
|
||||
std::vector<LiveVariable> 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<SourceStackFrame> Debugger::get_source_stack_frames(int max_frames) {
|
||||
std::vector<SourceStackFrame> 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<u64> 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<u64>(&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<u64>(&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<u64>(&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;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <queue>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
@@ -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<SourceLocation> 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<std::string> 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<BacktraceFrame> get_backtrace(u64 rip, u64 rsp, std::optional<std::string> dump_path);
|
||||
std::vector<BacktraceFrame> get_backtrace(u64 rip,
|
||||
u64 rsp,
|
||||
std::optional<std::string> dump_path,
|
||||
bool quiet = false);
|
||||
std::optional<SourceLocation> get_source_location(u32 goal_addr);
|
||||
std::optional<std::string> get_type_name_of_basic(u32 goal_addr);
|
||||
std::optional<std::string> get_symbol_name_for_value(u32 value) const;
|
||||
std::optional<std::string> get_symbol_name_at_address(u32 goal_addr) const;
|
||||
|
||||
std::vector<ResolvedBreakpoint> resolve_source_breakpoint(const std::string& filename,
|
||||
int line,
|
||||
int max_line_slide = 50);
|
||||
|
||||
std::vector<SourceStackFrame> get_source_stack_frames(int max_frames = 128);
|
||||
std::vector<LiveVariable> get_live_variables();
|
||||
|
||||
bool resume_from_break();
|
||||
|
||||
bool do_step(StepKind kind);
|
||||
|
||||
void set_stop_callback(std::function<void(xdbg::SignalInfo::Kind)> cb) {
|
||||
std::lock_guard<std::mutex> lock(m_stop_callback_mutex);
|
||||
m_stop_callback = std::move(cb);
|
||||
}
|
||||
|
||||
u64 get_normalized_rip() const;
|
||||
std::optional<u32> 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<void(xdbg::SignalInfo::Kind)> 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<u64> get_return_address_of_current_frame();
|
||||
void place_breakpoints();
|
||||
void remove_breakpoints();
|
||||
std::optional<SourceLocation> source_location_for_function_offset(const FunctionDebugInfo& func,
|
||||
u32 function_offset) const;
|
||||
|
||||
InstructionPointerInfo m_break_info;
|
||||
|
||||
listener::Listener* m_listener = nullptr;
|
||||
|
||||
@@ -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<Compiler>(
|
||||
game_version, emitter::InstructionSet::X86, std::make_optional(repl_config), username,
|
||||
std::make_unique<REPL::Wrapper>(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<Compiler>(
|
||||
game_version, emitter::InstructionSet::X86, std::make_optional(repl_config), username,
|
||||
std::make_unique<REPL::Wrapper>(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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user