mirror of
https://github.com/open-goal/jak-project
synced 2026-09-02 10:11:44 -04:00
de25f39439
Got OpenGOAL building and running natively on Apple Silicon. This is the rest of the port after the smaller arm64 emitter PRs. I was told pushing one big PR was okay. This covers goalc, the runtime, linker, kernel and the GOAL asm. The new paths have native tests. I also found four more emitter bugs while running it. They're in scalar sqrt, 128-bit stores, scalar max and indexed stores above 4 GB. Spent a while cleaning it up so it's easier to read. 4am gotta sleep lol Closes #3841 --------- Co-authored-by: Tyler Wilding <xtvaser@gmail.com>
1913 lines
57 KiB
C++
1913 lines
57 KiB
C++
/*!
|
|
* @file Debugger.h
|
|
* The OpenGOAL debugger.
|
|
* Uses xdbg functions to debug an OpenGOAL target.
|
|
*/
|
|
|
|
#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"
|
|
#include "common/util/FileUtil.h"
|
|
#include "common/util/Timer.h"
|
|
|
|
#include "goalc/debugger/disassemble.h"
|
|
#include "goalc/emitter/Register.h"
|
|
#include "goalc/listener/Listener.h"
|
|
|
|
#include "fmt/format.h"
|
|
|
|
/*!
|
|
* Is the target halted? If we don't know or aren't connected, returns false.
|
|
*/
|
|
bool Debugger::is_halted() const {
|
|
return m_context_valid && m_attached && !m_running;
|
|
}
|
|
|
|
/*!
|
|
* Is the target running and attached? Note that this returns false if it's running, but not
|
|
* attached to the debugger.
|
|
*/
|
|
bool Debugger::is_running() const {
|
|
return m_context_valid && m_attached && m_running;
|
|
}
|
|
|
|
/*!
|
|
* Do we have a valid debugging context? Without this we cannot attach or do any debugging.
|
|
*/
|
|
bool Debugger::is_valid() const {
|
|
return m_context_valid;
|
|
}
|
|
|
|
/*!
|
|
* Invalidate the current debugging context. For example if the target restarts.
|
|
*/
|
|
void Debugger::invalidate() {
|
|
m_context_valid = false;
|
|
}
|
|
|
|
/*!
|
|
* Are we attached to a valid target?
|
|
*/
|
|
bool Debugger::is_attached() const {
|
|
return m_context_valid && m_attached;
|
|
}
|
|
|
|
/*!
|
|
* If attached, detach. If halted and attached, will unhalt.
|
|
* Will silently do nothing if we aren't attached, so it is safe to just call detach() to try to
|
|
* clean up when exiting.
|
|
*/
|
|
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();
|
|
}
|
|
stop_watcher();
|
|
xdbg::close_memory(m_debug_context.tid, &m_memory_handle);
|
|
xdbg::detach_and_resume(m_debug_context.tid);
|
|
#elif _WIN32
|
|
if (is_halted()) {
|
|
succ = do_continue();
|
|
}
|
|
{
|
|
std::unique_lock<std::mutex> lk(m_watcher_mutex);
|
|
m_attach_return = false;
|
|
}
|
|
stop_watcher();
|
|
{
|
|
std::unique_lock<std::mutex> lk(m_watcher_mutex);
|
|
m_attach_cv.wait(lk, [&]() { return m_attach_return; });
|
|
}
|
|
xdbg::close_memory(m_debug_context.tid, &m_memory_handle);
|
|
#endif
|
|
// m_context_valid = false;
|
|
m_attached = false;
|
|
} else {
|
|
succ = false;
|
|
}
|
|
// todo, should we print something if we can't detach?
|
|
return succ;
|
|
}
|
|
|
|
/*!
|
|
* Set the debug context to allow Debugger to attach.
|
|
*/
|
|
void Debugger::set_context(u32 s7, uintptr_t base, const std::string& thread_id) {
|
|
m_debug_context.s7 = s7;
|
|
m_debug_context.base = base;
|
|
m_debug_context.tid = xdbg::ThreadID(thread_id);
|
|
m_context_valid = true;
|
|
}
|
|
|
|
/*!
|
|
* Get information about the context for debugging the debugger.
|
|
*/
|
|
std::string Debugger::get_context_string() const {
|
|
return fmt::format("valid = {}, s7 = 0x{:x}, base = 0x{:x}, tid = {}\n", is_valid(),
|
|
m_debug_context.s7, m_debug_context.base, m_debug_context.tid.to_string());
|
|
}
|
|
|
|
/*!
|
|
* Attach the debugger to the current context (must be valid) and break.
|
|
* Returns once the target actually stops.
|
|
*/
|
|
bool Debugger::attach_and_break() {
|
|
if (is_valid() && !m_attached) {
|
|
// reset and start the stop watcher
|
|
clear_signal_queue();
|
|
|
|
// attach and send a break command
|
|
if (try_start_watcher()) {
|
|
// wait for the signal queue to get a stop and pop it.
|
|
auto info = pop_signal();
|
|
|
|
// manually set up continue for this.
|
|
m_continue_info.valid = true;
|
|
m_continue_info.subtract_1 = false;
|
|
|
|
// this may fail if you crash at exactly the wrong time. todo - remove?
|
|
if (info.kind != xdbg::SignalInfo::BREAK) {
|
|
lg::print("[Debugger] got signal {} when expecting break.\n", (int)info.kind);
|
|
}
|
|
|
|
// open the memory of the process
|
|
if (!xdbg::open_memory(m_debug_context.tid, &m_memory_handle)) {
|
|
return false;
|
|
}
|
|
|
|
m_attached = true;
|
|
m_running = false;
|
|
|
|
// get info from target
|
|
update_break_info({});
|
|
|
|
auto signal_count = get_signal_count();
|
|
if (signal_count != 0) {
|
|
lg::print("[Debugger] got signal count of {} in attach_and_break\n", signal_count);
|
|
}
|
|
return true;
|
|
}
|
|
} else {
|
|
lg::print("[Debugger] attach_and_break can't be done when valid = {} and attached = {}\n",
|
|
is_valid(), m_attached);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
std::string Debugger::get_info_about_addr(u32 addr) {
|
|
if (addr >= EE_MAIN_MEM_LOW_PROTECT && addr < EE_MAIN_MEM_SIZE) {
|
|
auto map_loc = m_memory_map.lookup(addr);
|
|
if (map_loc.empty) {
|
|
return "Unknown Address";
|
|
}
|
|
std::string result = fmt::format("Object: {} {} (0x{:x} to 0x{:x}) offset 0x{:x}\n",
|
|
map_loc.obj_name, map_loc.seg_id, map_loc.start_addr,
|
|
map_loc.end_addr, addr - map_loc.start_addr);
|
|
u64 obj_offset = addr - map_loc.start_addr;
|
|
FunctionDebugInfo* info = nullptr;
|
|
std::string name;
|
|
if (get_debug_info_for_object(map_loc.obj_name)
|
|
.lookup_function(&info, &name, obj_offset, map_loc.seg_id)) {
|
|
result += fmt::format("Name: {}\n", name);
|
|
}
|
|
return result;
|
|
} else {
|
|
return "Outside of GOAL memory";
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* This assumes we have an up-to-date memory map and symbol info.
|
|
*/
|
|
InstructionPointerInfo Debugger::get_rip_info(u64 rip) {
|
|
InstructionPointerInfo result;
|
|
result.real_rip = rip;
|
|
|
|
if (m_context_valid) {
|
|
result.goal_rip = rip - m_debug_context.base;
|
|
if (rip >= m_debug_context.base + EE_MAIN_MEM_LOW_PROTECT &&
|
|
rip < m_debug_context.base + EE_MAIN_MEM_SIZE) {
|
|
result.in_goal_mem = true;
|
|
auto map_loc = m_memory_map.lookup(rip - m_debug_context.base);
|
|
if (map_loc.empty) {
|
|
result.knows_object = false;
|
|
result.knows_function = false;
|
|
} else {
|
|
u64 obj_offset = rip - m_debug_context.base - map_loc.start_addr;
|
|
result.map_entry = map_loc;
|
|
result.knows_object = true;
|
|
result.object_name = map_loc.obj_name;
|
|
result.object_seg = map_loc.seg_id;
|
|
result.object_offset = obj_offset;
|
|
|
|
FunctionDebugInfo* info = nullptr;
|
|
std::string name;
|
|
|
|
if (get_debug_info_for_object(map_loc.obj_name)
|
|
.lookup_function(&info, &name, obj_offset, map_loc.seg_id)) {
|
|
result.knows_function = true;
|
|
result.function_name = name;
|
|
result.function_offset = obj_offset - info->offset_in_seg;
|
|
result.func_debug = info;
|
|
|
|
ASSERT(!info->instructions.empty());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
std::vector<BacktraceFrame> Debugger::get_backtrace(u64 rip,
|
|
u64 rsp,
|
|
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 = "";
|
|
if (!quiet) {
|
|
lg::print("Backtrace:\n");
|
|
}
|
|
std::vector<BacktraceFrame> bt;
|
|
|
|
bool null_pc = rip == m_debug_context.base;
|
|
if (null_pc) {
|
|
// we jumped to NULL.
|
|
|
|
u64 next_rip = 0;
|
|
if (!read_memory_if_safe<u64>(&next_rip, rsp - m_debug_context.base)) {
|
|
if (!quiet) {
|
|
lg::print("Failed to read return address off of the stack!\n");
|
|
}
|
|
return {};
|
|
}
|
|
|
|
rip = next_rip;
|
|
rsp += 8;
|
|
}
|
|
|
|
int fails = 0;
|
|
while (true) {
|
|
std::string this_backtrace;
|
|
this_backtrace = fmt::format(" rsp: 0x{:x} (#x{:x}) rip: 0x{:x} (#x{:x})\n", rsp,
|
|
rsp - m_debug_context.base, rip, rip - m_debug_context.base);
|
|
BacktraceFrame frame;
|
|
frame.rip_info = get_rip_info(rip);
|
|
frame.rsp_at_rip = rsp;
|
|
|
|
if (frame.rip_info.knows_function && frame.rip_info.func_debug &&
|
|
frame.rip_info.func_debug->stack_usage) {
|
|
fails = 0;
|
|
this_backtrace += "<====================== CALL STACK ======================>\n";
|
|
this_backtrace += fmt::format("{} from {}\n", frame.rip_info.function_name,
|
|
frame.rip_info.func_debug->obj_name);
|
|
// we're good!
|
|
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;
|
|
if (!read_memory_if_safe<u64>(&next_rip, rsp_at_call - m_debug_context.base)) {
|
|
this_backtrace += "Invalid return address encountered!\n";
|
|
backtrace_contents = this_backtrace + backtrace_contents;
|
|
break;
|
|
}
|
|
|
|
rip = next_rip;
|
|
rsp = rsp_at_call + 8; // 8 for the call itself.
|
|
|
|
} else {
|
|
if (!frame.rip_info.knows_function) {
|
|
if (fails == 0) {
|
|
this_backtrace += "Unknown Function at rip\n";
|
|
}
|
|
|
|
/*
|
|
bool found = false;
|
|
if (s32(rip - m_debug_context.base) > 0 &&
|
|
m_symbol_name_to_value_map.find("function") != m_symbol_name_to_value_map.cend()) {
|
|
lg::print("Attempting to find function at this address.\n");
|
|
u32 function_sym_val = m_symbol_name_to_value_map.at("function");
|
|
u32 goal_pc = u32(rip - m_debug_context.base) & -8;
|
|
|
|
// go back through memory, but stop before reading the symbol table
|
|
u32 symtable_end = m_symbol_name_to_value_map.at("#f") + 0xff38;
|
|
while (goal_pc > symtable_end) {
|
|
goal_pc -= 8;
|
|
u32 wordval;
|
|
if (!read_memory_if_safe<u32>(&wordval, goal_pc)) {
|
|
goal_pc = symtable_end;
|
|
break;
|
|
}
|
|
|
|
if (wordval == function_sym_val) {
|
|
// found a function!
|
|
lg::print("Found function after {} bytes!\n",
|
|
(rip - m_debug_context.base) - goal_pc);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (goal_pc <= symtable_end) {
|
|
lg::print("Could not find function within this address.\n");
|
|
} else {
|
|
rip = goal_pc + m_debug_context.base + BASIC_OFFSET;
|
|
found = true;
|
|
}
|
|
} else*/
|
|
if (fails > 70) {
|
|
this_backtrace +=
|
|
"Backtrace was too long. Exception might have happened outside GOAL code, or the "
|
|
"stack frame is too long.\n";
|
|
backtrace_contents = this_backtrace + backtrace_contents;
|
|
break;
|
|
}
|
|
// attempt to backtrace anyway! if this fails then rip
|
|
u64 next_rip = 0;
|
|
if (!read_memory_if_safe<u64>(&next_rip, rsp - m_debug_context.base - 8)) {
|
|
this_backtrace += "Invalid return address encountered!\n";
|
|
backtrace_contents = this_backtrace + backtrace_contents;
|
|
break;
|
|
}
|
|
|
|
rip = next_rip;
|
|
rsp = rsp + 8; // 8 for the call itself.
|
|
++fails;
|
|
// break;
|
|
} else if (!frame.rip_info.func_debug) {
|
|
this_backtrace +=
|
|
fmt::format("Function {} has no debug info.\n", frame.rip_info.function_name);
|
|
backtrace_contents = this_backtrace + backtrace_contents;
|
|
break;
|
|
} else {
|
|
this_backtrace +=
|
|
fmt::format("Function {} with no stack frame data.\n", frame.rip_info.function_name);
|
|
backtrace_contents = this_backtrace + backtrace_contents;
|
|
break;
|
|
}
|
|
}
|
|
|
|
bt.push_back(frame);
|
|
|
|
backtrace_contents = this_backtrace + backtrace_contents;
|
|
}
|
|
|
|
if (!quiet) {
|
|
lg::print("{}\n", backtrace_contents);
|
|
}
|
|
if (dump_path) {
|
|
file_util::write_text_file(dump_path.value(), backtrace_contents);
|
|
}
|
|
|
|
return bt;
|
|
}
|
|
|
|
/*!
|
|
* This assumes we have an up-to-date memory map and symbol info.
|
|
*/
|
|
Disassembly Debugger::disassemble_at_rip(const InstructionPointerInfo& info) {
|
|
// todo adjust rip if break instruction????
|
|
Disassembly result;
|
|
|
|
result.failed = false;
|
|
u64 rip = info.real_rip;
|
|
|
|
if (info.in_goal_mem) {
|
|
// we only want to disassemble GOAL code.
|
|
// if the crash happens outside of GOAL code, use a normal debugger.
|
|
|
|
if (!info.knows_function || !info.knows_object || !info.map_entry) {
|
|
// something went wrong and we can't find this code.
|
|
// however, we can still do better than nothing by dumping the memory and disassembling.
|
|
std::vector<u8> mem;
|
|
mem.resize(INSTR_DUMP_SIZE_REV + INSTR_DUMP_SIZE_FWD);
|
|
read_memory(mem.data(), INSTR_DUMP_SIZE_REV + INSTR_DUMP_SIZE_FWD,
|
|
info.real_rip - m_debug_context.base - INSTR_DUMP_SIZE_REV);
|
|
result.failed = true;
|
|
if (info.knows_object) {
|
|
result.text += fmt::format("In segment {} of obj {}, offset 0x{:x}\n", info.object_seg,
|
|
info.object_name, info.object_offset);
|
|
result.text += disassemble_x86(mem.data(), mem.size(), rip - INSTR_DUMP_SIZE_REV, rip);
|
|
} else {
|
|
result.text += "In unknown code\n";
|
|
result.text += disassemble_x86(mem.data(), mem.size(), rip - INSTR_DUMP_SIZE_REV, rip);
|
|
}
|
|
} else {
|
|
// we have enough info to do a fancy disassembly!
|
|
u64 obj_offset = rip - m_debug_context.base - info.map_entry->start_addr;
|
|
|
|
FunctionDebugInfo* func_info = info.func_debug;
|
|
std::string name = func_info->name;
|
|
auto continue_info = get_continue_info(rip);
|
|
ASSERT(!func_info->instructions.empty());
|
|
|
|
std::vector<u8> function_mem;
|
|
function_mem.resize(func_info->instructions.back().offset +
|
|
func_info->instructions.back().instruction.length());
|
|
read_memory(function_mem.data(), function_mem.size(),
|
|
info.map_entry->start_addr + func_info->offset_in_seg);
|
|
|
|
int rip_offset = 0;
|
|
if (continue_info.valid && continue_info.is_addr_breakpiont) {
|
|
int offset_in_fmem = uint64_t(continue_info.addr_breakpoint.goal_addr) -
|
|
uint64_t(info.map_entry->start_addr + func_info->offset_in_seg);
|
|
if (offset_in_fmem < 0 || offset_in_fmem >= int(function_mem.size())) {
|
|
result.failed = true;
|
|
} else {
|
|
function_mem.at(offset_in_fmem) = continue_info.addr_breakpoint.old_data;
|
|
rip_offset = -1;
|
|
}
|
|
}
|
|
|
|
result.text += fmt::format(
|
|
"In function {} in segment {} of obj {}, offset_obj 0x{:x}, offset_func 0x{:x}\n", name,
|
|
info.map_entry->seg_id, info.map_entry->obj_name, obj_offset, info.function_offset);
|
|
|
|
result.text += disassemble_x86_function(
|
|
function_mem.data(), function_mem.size(), m_reader,
|
|
m_debug_context.base + info.map_entry->start_addr + func_info->offset_in_seg,
|
|
rip + rip_offset, func_info->instructions, func_info->code_sources, func_info->ir_strings,
|
|
&result.failed, false, false);
|
|
}
|
|
} else {
|
|
result.failed = true;
|
|
result.text = "Not in GOAL code!\n";
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/*!
|
|
* Read the registers, symbol table, and instructions near rip.
|
|
* Print out some info about where we are.
|
|
*/
|
|
void Debugger::reload_break_state() {
|
|
m_memory_map = m_listener->build_memory_map();
|
|
// lg::print("{}", m_memory_map.print());
|
|
read_symbol_table();
|
|
// xdbg::Regs uses the x86 register layout and the macOS backend is not implemented.
|
|
m_regs_valid = xdbg::get_regs_now(m_debug_context.tid, &m_regs_at_break);
|
|
|
|
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);
|
|
}
|
|
|
|
if (!m_regs_valid) {
|
|
lg::print("[Debugger] get_regs_now failed after break, something is wrong\n");
|
|
} else {
|
|
lg::print("{}", m_regs_at_break.print_gprs());
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* Stop the target. Must be attached and not stopped.
|
|
* Waits for break to be acknowledged and reads break info.
|
|
*/
|
|
bool Debugger::do_break() {
|
|
ASSERT(is_valid() && is_attached() && is_running());
|
|
m_expecting_immeidate_break = true;
|
|
m_continue_info.valid = false;
|
|
clear_signal_queue();
|
|
if (!xdbg::break_now(m_debug_context.tid)) {
|
|
return false;
|
|
} else {
|
|
auto info = pop_signal();
|
|
ASSERT(info.kind == xdbg::SignalInfo::BREAK);
|
|
update_break_info({});
|
|
m_running = false;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* Continue the target, must be attached and stopped.
|
|
*/
|
|
bool Debugger::do_continue() {
|
|
ASSERT(is_valid() && is_attached() && is_halted());
|
|
if (!m_regs_valid) {
|
|
update_break_info({});
|
|
}
|
|
ASSERT(regs_valid());
|
|
|
|
return resume_from_break();
|
|
}
|
|
|
|
/*!
|
|
* Read memory from an attached and halted target.
|
|
*/
|
|
bool Debugger::read_memory(u8* dest_buffer, int size, u32 goal_addr) const {
|
|
ASSERT(is_valid() && is_attached() && is_halted());
|
|
return xdbg::read_goal_memory(dest_buffer, size, goal_addr, m_debug_context, m_memory_handle);
|
|
}
|
|
|
|
bool Debugger::read_memory_if_safe(u8* dest_buffer, int size, u32 goal_addr) const {
|
|
ASSERT(is_valid() && is_attached() && is_halted());
|
|
if (goal_addr >= EE_MAIN_MEM_LOW_PROTECT && goal_addr + size < EE_MAIN_MEM_SIZE) {
|
|
return read_memory(dest_buffer, size, goal_addr);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/*!
|
|
* Write the memory of an attached and halted target.
|
|
*/
|
|
bool Debugger::write_memory(const u8* src_buffer, int size, u32 goal_addr) {
|
|
ASSERT(is_valid() && is_attached() && is_halted());
|
|
return xdbg::write_goal_memory(src_buffer, size, goal_addr, m_debug_context, m_memory_handle);
|
|
}
|
|
|
|
void Debugger::read_symbol_table_jak1() {
|
|
using namespace jak1_symbols;
|
|
using namespace jak1;
|
|
ASSERT(is_valid() && is_attached() && is_halted());
|
|
u32 bytes_read = 0;
|
|
u32 reads = 0;
|
|
Timer timer;
|
|
|
|
u32 st_base = m_debug_context.s7 - ((GOAL_MAX_SYMBOLS / 2) * 8 + BASIC_OFFSET);
|
|
u32 empty_pair_offset = (m_debug_context.s7 + FIX_SYM_EMPTY_PAIR - PAIR_OFFSET) - st_base;
|
|
|
|
std::vector<u8> mem;
|
|
mem.resize(SYM_TABLE_MEM_SIZE);
|
|
|
|
if (!xdbg::read_goal_memory(mem.data(), SYM_TABLE_MEM_SIZE, st_base, m_debug_context,
|
|
m_memory_handle)) {
|
|
lg::print("Read failed during read_symbol_table\n");
|
|
return;
|
|
}
|
|
reads++;
|
|
bytes_read += SYM_TABLE_MEM_SIZE;
|
|
|
|
struct SymLower {
|
|
u32 type;
|
|
u32 value;
|
|
};
|
|
|
|
struct SymUpper {
|
|
u32 hash;
|
|
u32 str;
|
|
};
|
|
|
|
m_symbol_name_to_offset_map.clear();
|
|
m_symbol_offset_to_name_map.clear();
|
|
m_symbol_name_to_value_map.clear();
|
|
|
|
u32 sym_type = 0;
|
|
// now loop through all the symbols
|
|
for (int i = 0; i < (SYM_INFO_OFFSET + 4) / int(sizeof(SymLower)); i++) {
|
|
auto offset = i * sizeof(SymLower);
|
|
if (offset == empty_pair_offset) {
|
|
continue;
|
|
}
|
|
auto sym = (SymLower*)(mem.data() + offset);
|
|
if (sym->type) {
|
|
// got a symbol!
|
|
if (!sym_type) {
|
|
sym_type = sym->type;
|
|
} else {
|
|
if (sym_type != sym->type) {
|
|
lg::print("Got bad symbol type. Expected 0x{:x} got 0x{:x}: addr 0x{:x}\n", sym_type,
|
|
sym->type, offset + st_base + (uint64_t)m_debug_context.base);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// now get the info
|
|
auto info = (SymUpper*)(mem.data() + i * sizeof(SymLower) + SYM_INFO_OFFSET + BASIC_OFFSET);
|
|
|
|
// now get the string.
|
|
char str_buff[128];
|
|
if (!xdbg::read_goal_memory((u8*)str_buff, 128, info->str + 4, m_debug_context,
|
|
m_memory_handle)) {
|
|
lg::print("Read symbol string failed during read_symbol_table\n");
|
|
return;
|
|
}
|
|
reads++;
|
|
bytes_read += 128;
|
|
// just in case
|
|
str_buff[127] = '\0';
|
|
|
|
// GOAL sym - s7
|
|
auto sym_offset = s32(offset + st_base + BASIC_OFFSET) - s32(m_debug_context.s7);
|
|
ASSERT(sym_offset >= -SYM_TABLE_MEM_SIZE / 4);
|
|
ASSERT(sym_offset < SYM_TABLE_MEM_SIZE / 4);
|
|
|
|
std::string str(str_buff);
|
|
if (str.length() >= 50) {
|
|
lg::print("Invalid symbol #x{:x}!\n", sym_offset);
|
|
continue;
|
|
}
|
|
|
|
// update maps
|
|
if (m_symbol_name_to_offset_map.find(str) != m_symbol_name_to_offset_map.end()) {
|
|
if (str == "asize-of-basic-func") {
|
|
// this is an actual bug in kscheme. The bug has no effect, but we replicate it so that
|
|
// the symbol table layout is closer.
|
|
|
|
// to hide this duplicate symbol, we append "-hack-copy" to the end of it.
|
|
str += "-hack-copy";
|
|
} else {
|
|
lg::print("Symbol {} (#x{:x}) appears multiple times!\n", str, sym_offset);
|
|
continue;
|
|
// ASSERT(false);
|
|
}
|
|
}
|
|
|
|
m_symbol_name_to_offset_map[str] = sym_offset;
|
|
m_symbol_offset_to_name_map[sym_offset] = str;
|
|
m_symbol_name_to_value_map[str] = sym->value;
|
|
}
|
|
}
|
|
|
|
ASSERT(m_symbol_offset_to_name_map.size() == m_symbol_name_to_offset_map.size());
|
|
lg::print("Read symbol table ({} bytes, {} reads, {} symbols, {:.2f} ms)\n", bytes_read, reads,
|
|
m_symbol_name_to_offset_map.size(), timer.getMs());
|
|
}
|
|
|
|
void Debugger::read_symbol_table_jak2() {
|
|
using namespace jak2_symbols;
|
|
using namespace jak2;
|
|
ASSERT(is_valid() && is_attached() && is_halted());
|
|
u32 bytes_read = 0;
|
|
u32 reads = 0;
|
|
Timer timer;
|
|
|
|
u32 st_base = m_debug_context.s7 - ((GOAL_MAX_SYMBOLS / 2) * 4 + 1);
|
|
u32 empty_pair_offset =
|
|
(m_debug_context.s7 + S7_OFF_FIX_SYM_EMPTY_PAIR /*- PAIR_OFFSET*/) - st_base;
|
|
|
|
std::vector<u8> mem;
|
|
mem.resize(SYM_TABLE_MEM_SIZE);
|
|
|
|
if (!xdbg::read_goal_memory(mem.data(), SYM_TABLE_MEM_SIZE, st_base, m_debug_context,
|
|
m_memory_handle)) {
|
|
lg::print("Read failed during read_symbol_table\n");
|
|
return;
|
|
}
|
|
reads++;
|
|
bytes_read += SYM_TABLE_MEM_SIZE;
|
|
|
|
m_symbol_name_to_offset_map.clear();
|
|
m_symbol_offset_to_name_map.clear();
|
|
m_symbol_name_to_value_map.clear();
|
|
|
|
// now loop through all the symbols
|
|
for (int i = 0; i < (SYM_TO_STRING_OFFSET + 4) / 4; i++) {
|
|
u32 offset = i * 4;
|
|
if (offset == empty_pair_offset) {
|
|
continue;
|
|
}
|
|
auto sym_val = *(u32*)(mem.data() + offset);
|
|
auto info = *(u32*)(mem.data() + i * 4 + SYM_TO_STRING_OFFSET + 1);
|
|
if (info) {
|
|
// now get the string.
|
|
char str_buff[128];
|
|
if (!xdbg::read_goal_memory((u8*)str_buff, 128, info + 4, m_debug_context, m_memory_handle)) {
|
|
lg::print("Read symbol string failed during read_symbol_table\n");
|
|
return;
|
|
}
|
|
reads++;
|
|
bytes_read += 128;
|
|
// just in case
|
|
str_buff[127] = '\0';
|
|
|
|
// GOAL sym - s7
|
|
auto sym_offset = s32(offset + st_base) - s32(m_debug_context.s7);
|
|
ASSERT(sym_offset >= -SYM_TABLE_MEM_SIZE / 4);
|
|
ASSERT(sym_offset < SYM_TABLE_MEM_SIZE / 4);
|
|
|
|
std::string str(str_buff);
|
|
if (str.length() >= 50) {
|
|
lg::print("Invalid symbol #x{:x}!\n", sym_offset);
|
|
continue;
|
|
}
|
|
|
|
// update maps
|
|
if (m_symbol_name_to_offset_map.find(str) != m_symbol_name_to_offset_map.end()) {
|
|
if (str == "asize-of-basic-func") {
|
|
// this is an actual bug in kscheme. The bug has no effect, but we replicate it so that
|
|
// the symbol table layout is closer.
|
|
|
|
// to hide this duplicate symbol, we append "-hack-copy" to the end of it.
|
|
str += "-hack-copy";
|
|
} else {
|
|
lg::print("Symbol {} (#x{:x}) appears multiple times!\n", str, sym_offset);
|
|
continue;
|
|
// ASSERT(false);
|
|
}
|
|
}
|
|
|
|
m_symbol_name_to_offset_map[str] = sym_offset;
|
|
m_symbol_offset_to_name_map[sym_offset] = str;
|
|
m_symbol_name_to_value_map[str] = sym_val;
|
|
}
|
|
}
|
|
|
|
ASSERT(m_symbol_offset_to_name_map.size() == m_symbol_name_to_offset_map.size());
|
|
lg::print("Read symbol table ({} bytes, {} reads, {} symbols, {:.2f} ms)\n", bytes_read, reads,
|
|
m_symbol_name_to_offset_map.size(), timer.getMs());
|
|
}
|
|
|
|
void Debugger::read_symbol_table_jak3() {
|
|
using namespace jak3_symbols;
|
|
using namespace jak3;
|
|
ASSERT(is_valid() && is_attached() && is_halted());
|
|
u32 bytes_read = 0;
|
|
u32 reads = 0;
|
|
Timer timer;
|
|
|
|
constexpr int kS7Offset = ((GOAL_MAX_SYMBOLS / 2) * 4 + 1);
|
|
static_assert(kS7Offset == 0x8001); // this is what we have hardcoded now
|
|
u32 st_base = m_debug_context.s7 - kS7Offset;
|
|
u32 empty_pair_offset =
|
|
(m_debug_context.s7 + S7_OFF_FIX_SYM_EMPTY_PAIR /*- PAIR_OFFSET*/) - st_base;
|
|
|
|
constexpr u32 kSymbolMemSize = 2 * (GOAL_MAX_SYMBOLS * 4); // symbol, then strings.
|
|
std::vector<u8> mem;
|
|
mem.resize(kSymbolMemSize);
|
|
|
|
if (!xdbg::read_goal_memory(mem.data(), kSymbolMemSize, st_base, m_debug_context,
|
|
m_memory_handle)) {
|
|
lg::print("Read failed during read_symbol_table\n");
|
|
return;
|
|
}
|
|
reads++;
|
|
bytes_read += kSymbolMemSize;
|
|
|
|
m_symbol_name_to_offset_map.clear();
|
|
m_symbol_offset_to_name_map.clear();
|
|
m_symbol_name_to_value_map.clear();
|
|
|
|
// now loop through all the symbols
|
|
for (int i = 0; i < GOAL_MAX_SYMBOLS; i++) {
|
|
u32 offset = i * 4;
|
|
if (offset == empty_pair_offset) {
|
|
continue;
|
|
}
|
|
auto sym_val = *(u32*)(mem.data() + offset);
|
|
auto info = *(u32*)(mem.data() + offset + kSymbolMemSize / 2);
|
|
if (info) {
|
|
// now get the string.
|
|
char str_buff[128];
|
|
if (!xdbg::read_goal_memory((u8*)str_buff, 128, info + 4, m_debug_context, m_memory_handle)) {
|
|
lg::print("Read symbol string failed during read_symbol_table\n");
|
|
return;
|
|
}
|
|
reads++;
|
|
bytes_read += 128;
|
|
// just in case
|
|
str_buff[127] = '\0';
|
|
|
|
// GOAL sym - s7
|
|
auto sym_offset = s32(offset + st_base) - s32(m_debug_context.s7);
|
|
// ASSERT(sym_offset >= -SYM_TABLE_MEM_SIZE / 4);
|
|
// ASSERT(sym_offset < SYM_TABLE_MEM_SIZE / 4);
|
|
|
|
std::string str(str_buff);
|
|
if (str.length() >= 60) {
|
|
lg::print("Invalid symbol #x{:x}!\n", sym_offset);
|
|
continue;
|
|
}
|
|
|
|
// update maps
|
|
if (m_symbol_name_to_offset_map.find(str) != m_symbol_name_to_offset_map.end()) {
|
|
if (str == "asize-of-basic-func") {
|
|
// this is an actual bug in kscheme. The bug has no effect, but we replicate it so that
|
|
// the symbol table layout is closer.
|
|
|
|
// to hide this duplicate symbol, we append "-hack-copy" to the end of it.
|
|
str += "-hack-copy";
|
|
} else {
|
|
lg::print("Symbol {} (#x{:x}) appears multiple times!\n", str, sym_offset);
|
|
continue;
|
|
// ASSERT(false);
|
|
}
|
|
}
|
|
|
|
m_symbol_name_to_offset_map[str] = sym_offset;
|
|
m_symbol_offset_to_name_map[sym_offset] = str;
|
|
m_symbol_name_to_value_map[str] = sym_val;
|
|
}
|
|
}
|
|
|
|
ASSERT(m_symbol_offset_to_name_map.size() == m_symbol_name_to_offset_map.size());
|
|
lg::print("Read symbol table ({} bytes, {} reads, {} symbols, {:.2f} ms)\n", bytes_read, reads,
|
|
m_symbol_name_to_offset_map.size(), timer.getMs());
|
|
}
|
|
|
|
/*!
|
|
* Read the GOAL Symbol table from an attached and halted target.
|
|
*/
|
|
void Debugger::read_symbol_table() {
|
|
switch (m_version) {
|
|
case GameVersion::Jak1:
|
|
read_symbol_table_jak1();
|
|
break;
|
|
case GameVersion::Jak2:
|
|
read_symbol_table_jak2();
|
|
break;
|
|
case GameVersion::Jak3:
|
|
read_symbol_table_jak3();
|
|
break;
|
|
default:
|
|
ASSERT(false);
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* Get the address of a symbol by name. Returns a GOAL address.
|
|
* Returns 0 if the symbol doesn't exist.
|
|
*/
|
|
u32 Debugger::get_symbol_address(const std::string& sym_name) {
|
|
ASSERT(is_valid());
|
|
auto kv = m_symbol_name_to_offset_map.find(sym_name);
|
|
if (kv != m_symbol_name_to_offset_map.end()) {
|
|
return m_debug_context.s7 + kv->second;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*!
|
|
* Get the value of a symbol by name. Returns if the symbol exists and populates output if it does.
|
|
*/
|
|
bool Debugger::get_symbol_value(const std::string& sym_name, u32* output) {
|
|
ASSERT(is_valid());
|
|
auto kv = m_symbol_name_to_value_map.find(sym_name);
|
|
if (kv != m_symbol_name_to_value_map.end()) {
|
|
*output = kv->second;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/*!
|
|
* Get the value of a symbol by name. Returns NULL if symbol does not exist.
|
|
*/
|
|
const char* Debugger::get_symbol_name_from_offset(s32 ofs) const {
|
|
ASSERT(is_valid());
|
|
auto kv = m_symbol_offset_to_name_map.find(ofs);
|
|
if (kv != m_symbol_offset_to_name_map.end()) {
|
|
return kv->second.c_str();
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/*!
|
|
* Attempt to start the debugger watch thread and evaluate attach success. Stops if unsuccessful.
|
|
*/
|
|
bool Debugger::try_start_watcher() {
|
|
#ifdef __linux
|
|
m_attach_response = xdbg::attach_and_break(m_debug_context.tid);
|
|
if (!m_attach_response)
|
|
return false;
|
|
start_watcher();
|
|
return true;
|
|
#elif defined(_WIN32)
|
|
start_watcher();
|
|
std::unique_lock<std::mutex> lk(m_watcher_mutex);
|
|
m_attach_cv.wait(lk, [&]() { return m_attach_return; });
|
|
if (!m_attach_response) {
|
|
stop_watcher();
|
|
}
|
|
return m_attach_response;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
/*!
|
|
* Starts the debugger watch thread which watches the target process to see if it stops.
|
|
*/
|
|
void Debugger::start_watcher() {
|
|
if (m_watcher_running) {
|
|
stop_watcher();
|
|
}
|
|
ASSERT(!m_watcher_running);
|
|
m_watcher_running = true;
|
|
m_watcher_should_stop = false;
|
|
{
|
|
std::unique_lock<std::mutex> lk(m_watcher_mutex);
|
|
m_attach_return = false;
|
|
}
|
|
m_watcher_thread = std::thread(&Debugger::watcher, this);
|
|
}
|
|
|
|
/*!
|
|
* Stops the debugger watch thread (waits for it to end)
|
|
*/
|
|
void Debugger::stop_watcher() {
|
|
ASSERT(m_watcher_running);
|
|
m_watcher_running = false;
|
|
m_watcher_should_stop = true;
|
|
m_watcher_thread.join();
|
|
}
|
|
|
|
Debugger::~Debugger() {
|
|
if (m_watcher_running) {
|
|
stop_watcher();
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* The watcher thread.
|
|
*/
|
|
void Debugger::watcher() {
|
|
// watcher will now attach to target.
|
|
// linux doesn't require the attachment and watching to be on the same thread, but windows does.
|
|
#ifdef _WIN32
|
|
m_attach_response = xdbg::attach_and_break(m_debug_context.tid);
|
|
m_attach_return = true;
|
|
m_attach_cv.notify_all();
|
|
if (!m_attach_response)
|
|
return;
|
|
#endif
|
|
|
|
xdbg::SignalInfo signal_info;
|
|
while (!m_watcher_should_stop) {
|
|
// we just sit in a loop, waiting for stops.
|
|
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:
|
|
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");
|
|
break;
|
|
case xdbg::SignalInfo::DISAPPEARED:
|
|
printf("Target has disappeared. Maybe it quit or was killed.\n");
|
|
handle_disappearance();
|
|
break;
|
|
case xdbg::SignalInfo::ILLEGAL_INSTR:
|
|
printf(
|
|
"Target has crashed due to an illegal instruction. Run (:di) to get more "
|
|
"information.\n");
|
|
break;
|
|
case xdbg::SignalInfo::UNKNOWN:
|
|
printf("Target has encountered an unknown signal. Run (:di) to get more information.\n");
|
|
break;
|
|
#ifdef _WIN32
|
|
case xdbg::SignalInfo::EXCEPTION:
|
|
printf("Target raised an exception (%s). Run (:di) to get more information.\n",
|
|
signal_info.msg.c_str());
|
|
break;
|
|
case xdbg::SignalInfo::NOTHING:
|
|
// printf("Nothing happened.\n");
|
|
break;
|
|
#endif
|
|
default:
|
|
ASSERT_MSG(false, fmt::format("[Debugger] unhandled signal in watcher: {}",
|
|
int(signal_info.kind)));
|
|
}
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_watcher_mutex);
|
|
m_running = false;
|
|
m_watcher_queue.push({signal_info.kind}); // todo, more info?
|
|
}
|
|
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));
|
|
}
|
|
}
|
|
|
|
// watcher will now detach from target.
|
|
// again, windows needs the debugger thread to remain consistent
|
|
#ifdef _WIN32
|
|
m_attach_response = xdbg::detach_and_resume(m_debug_context.tid);
|
|
m_attach_return = true;
|
|
#endif
|
|
}
|
|
|
|
void Debugger::handle_disappearance() {
|
|
m_watcher_should_stop = true;
|
|
xdbg::close_memory(m_debug_context.tid, &m_memory_handle);
|
|
xdbg::detach_and_resume(m_debug_context.tid);
|
|
m_context_valid = false;
|
|
m_attached = false;
|
|
}
|
|
|
|
Debugger::SignalInfo Debugger::pop_signal() {
|
|
{
|
|
std::unique_lock<std::mutex> lock(m_watcher_mutex);
|
|
m_watcher_cv.wait(lock, [&] { return !m_watcher_queue.empty(); });
|
|
}
|
|
|
|
Debugger::SignalInfo result;
|
|
if (!try_pop_signal(&result)) {
|
|
ASSERT(false);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool Debugger::try_pop_signal(SignalInfo* out) {
|
|
{
|
|
std::unique_lock<std::mutex> lock(m_watcher_mutex);
|
|
if (!m_watcher_queue.empty()) {
|
|
*out = m_watcher_queue.front();
|
|
m_watcher_queue.pop();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
int Debugger::get_signal_count() {
|
|
std::unique_lock<std::mutex> lock(m_watcher_mutex);
|
|
return int(m_watcher_queue.size());
|
|
}
|
|
|
|
void Debugger::clear_signal_queue() {
|
|
std::unique_lock<std::mutex> lock(m_watcher_mutex);
|
|
while (!m_watcher_queue.empty()) {
|
|
m_watcher_queue.pop();
|
|
}
|
|
}
|
|
|
|
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);
|
|
if (kv != m_addr_breakpoints.end()) {
|
|
lg::print("Breakpoint at address 0x{:08x} already exists as breakpoint {}\n", addr,
|
|
kv->second.id);
|
|
return;
|
|
}
|
|
|
|
Breakpoint bp;
|
|
bp.goal_addr = addr;
|
|
bp.id = m_addr_breakpoints.size();
|
|
if (!read_memory(&bp.old_data, 1, addr)) {
|
|
lg::print("Failed to read memory for breakpoint, not adding breakpoint\n");
|
|
return;
|
|
}
|
|
|
|
u8 int3 = 0xcc;
|
|
if (!write_memory(&int3, 1, addr)) {
|
|
lg::print("Failed to write memory for breakpoint, not adding breakpoint\n");
|
|
return;
|
|
}
|
|
|
|
m_addr_breakpoints[addr] = bp;
|
|
}
|
|
}
|
|
|
|
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();
|
|
auto kv = m_addr_breakpoints.find(addr);
|
|
if (kv == m_addr_breakpoints.end()) {
|
|
lg::print("Breakpoint at address 0x{:08x} does not exist\n", addr);
|
|
return;
|
|
}
|
|
|
|
if (!write_memory(&kv->second.old_data, 1, addr)) {
|
|
lg::print("Failed to remove breakpoint\n");
|
|
return;
|
|
}
|
|
|
|
m_addr_breakpoints.erase(kv);
|
|
}
|
|
}
|
|
|
|
void Debugger::update_continue_info() {
|
|
if (m_continue_info.valid || !is_halted()) {
|
|
return;
|
|
}
|
|
|
|
if (!m_regs_valid) {
|
|
update_break_info({});
|
|
}
|
|
|
|
auto kv = m_addr_breakpoints.find(get_regs().rip - m_debug_context.base - 1);
|
|
if (kv == m_addr_breakpoints.end()) {
|
|
m_continue_info.subtract_1 = false;
|
|
m_continue_info.is_addr_breakpiont = false;
|
|
} else {
|
|
if (m_expecting_immeidate_break) {
|
|
printf("Warning, conflicting break and breakpoints. Not sure why we stopped!\n");
|
|
}
|
|
|
|
m_continue_info.subtract_1 = true;
|
|
m_continue_info.is_addr_breakpiont = true;
|
|
m_continue_info.addr_breakpoint = kv->second;
|
|
}
|
|
|
|
m_expecting_immeidate_break = false;
|
|
m_continue_info.valid = true;
|
|
}
|
|
|
|
Debugger::ContinueInfo Debugger::get_continue_info(u64 rip) const {
|
|
ContinueInfo result;
|
|
auto kv = m_addr_breakpoints.find(rip - m_debug_context.base - 1);
|
|
if (kv == m_addr_breakpoints.end()) {
|
|
result.subtract_1 = false;
|
|
result.is_addr_breakpiont = false;
|
|
} else {
|
|
result.subtract_1 = true;
|
|
result.is_addr_breakpiont = true;
|
|
result.addr_breakpoint = kv->second;
|
|
}
|
|
|
|
result.valid = true;
|
|
return result;
|
|
}
|
|
|
|
DebugInfo& Debugger::get_debug_info_for_object(const std::string& object_name) {
|
|
auto kv = m_debug_info.find(object_name);
|
|
if (kv != m_debug_info.end()) {
|
|
return kv->second;
|
|
}
|
|
|
|
return m_debug_info.insert(std::make_pair(object_name, DebugInfo(object_name))).first->second;
|
|
}
|
|
|
|
bool Debugger::knows_object(const std::string& object_name) const {
|
|
return m_debug_info.find(object_name) != m_debug_info.end();
|
|
}
|
|
|
|
/*!
|
|
* Do x86 disassembly at the specified address and then do some basic string replacement for
|
|
* symbols. It will attempt to detect symbol dereferences (e.g. *active-pool*), symbol references
|
|
* (e.g. 'dead), and a special case to detect #f (outputted as '#f for correctness).
|
|
*/
|
|
std::string Debugger::disassemble_x86_with_symbols(int len, u64 base_addr) const {
|
|
std::vector<u8> mem;
|
|
mem.resize(len);
|
|
|
|
read_memory(mem.data(), len, base_addr);
|
|
|
|
auto result = disassemble_x86(mem.data(), mem.size(), get_x86_base_addr() + base_addr);
|
|
|
|
// find symbol values!
|
|
const std::string sym_val_string("[r15+r14*1");
|
|
size_t pos = 0;
|
|
while ((pos = result.find(sym_val_string, pos)) != std::string::npos) {
|
|
size_t read;
|
|
auto sym_addr = std::stol(result.substr(pos + sym_val_string.length(), 7), &read,
|
|
16); // -0x1234 is 7 characters
|
|
|
|
auto sym_name = get_symbol_name_from_offset((s32)sym_addr);
|
|
if (sym_name) {
|
|
std::string sym_str(sym_name);
|
|
result.replace(pos + 1, read + sym_val_string.length() - 1,
|
|
sym_str); // the [ is ignored (result is something like: [identity])
|
|
pos += sym_str.length() + 1;
|
|
ASSERT(result.at(pos) == ']'); // maybe?
|
|
} else {
|
|
// symbol not found for whatever reason, just use regular disassembly and skip over
|
|
pos += 1;
|
|
}
|
|
}
|
|
|
|
// find symbol references!
|
|
const std::string sym_addr_string("[r14");
|
|
pos = 0;
|
|
while ((pos = result.find(sym_addr_string, pos)) != std::string::npos) {
|
|
size_t read;
|
|
auto sym_addr = std::stol(result.substr(pos + sym_addr_string.length(), 7), &read,
|
|
16); // -0x1234 is 7 characters
|
|
|
|
auto sym_name = get_symbol_name_from_offset((s32)sym_addr);
|
|
if (sym_name) {
|
|
std::string sym_str(sym_name);
|
|
result.replace(pos, read + sym_addr_string.length() + 1, fmt::format("'{}", sym_str));
|
|
pos += sym_str.length();
|
|
} else {
|
|
// symbol not found for whatever reason, just use regular disassembly and skip over
|
|
pos += 1;
|
|
}
|
|
}
|
|
|
|
// find #f references!
|
|
const std::string op_mov_string("] mov ");
|
|
const std::string sym_false_string(", r14");
|
|
pos = 0;
|
|
while ((pos = result.find(op_mov_string, pos)) != std::string::npos) {
|
|
pos += op_mov_string.length();
|
|
auto r14_pos = result.find(sym_false_string, pos);
|
|
if (r14_pos < result.find(op_mov_string, pos)) {
|
|
result.replace(r14_pos, sym_false_string.length(), fmt::format(", '#f"));
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|