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:
Hat Kid
2026-07-27 22:28:28 +02:00
committed by GitHub
parent e0dc11554d
commit fe2086acfb
17 changed files with 2515 additions and 42 deletions
+714 -29
View File
@@ -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;
}