/*! * @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 lk(m_watcher_mutex); m_attach_return = false; } stop_watcher(); { std::unique_lock 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 Debugger::get_backtrace(u64 rip, u64 rsp, std::optional dump_path, bool quiet) { // TODO - it would probably be nice to decouple printing the backtrace from getting the backtrace // for now, build up a string and dump it at the end (if a path is provided) std::string backtrace_contents = ""; if (!quiet) { lg::print("Backtrace:\n"); } std::vector 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(&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(&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(&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(&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 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 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(); 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 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 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 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 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 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 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 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 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 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 lock(m_watcher_mutex); return int(m_watcher_queue.size()); } void Debugger::clear_signal_queue() { std::unique_lock 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 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 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 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 cb; { std::lock_guard lock(m_stop_callback_mutex); cb = m_stop_callback; } if (cb) { cb(kind); } } u64 Debugger::get_normalized_rip() const { const u64 rip = m_regs_at_break.rip; if (!m_context_valid) { return rip; } if (m_addr_breakpoints.find(u32(rip - m_debug_context.base - 1)) != m_addr_breakpoints.end()) { return rip - 1; } return rip; } std::optional Debugger::get_breakpoint_addr_at_stop() const { if (!m_context_valid || !m_regs_valid) { return {}; } const u32 addr = u32(m_regs_at_break.rip - m_debug_context.base - 1); if (m_addr_breakpoints.find(addr) != m_addr_breakpoints.end()) { return addr; } return {}; } std::optional Debugger::source_location_for_function_offset( const FunctionDebugInfo& func, u32 function_offset) const { if (!m_reader) { return {}; } // find the last instruction at or before this offset with src info int best_ir = -1; int best_offset = -1; for (const auto& instr : func.instructions) { if (instr.kind != InstructionInfo::Kind::IR || instr.offset < 0) { continue; } if (u32(instr.offset) > function_offset) { continue; } if (instr.offset >= best_offset) { best_offset = instr.offset; best_ir = instr.ir_idx; } } if (best_ir < 0 || best_ir >= int(func.code_sources.size())) { return {}; } auto info = m_reader->db.try_get_short_info(func.code_sources.at(best_ir), false); if (!info) { return {}; } SourceLocation loc; loc.filename = info->filename; loc.line = info->line_idx_to_display; loc.column = info->pos_in_line; loc.line_text = info->line_text; return loc; } std::optional Debugger::get_symbol_name_for_value(u32 value) const { if (value == 0) { return {}; } for (const auto& [name, sym_value] : m_symbol_name_to_value_map) { if (sym_value == value) { return name; } } return {}; } std::optional Debugger::get_symbol_name_at_address(u32 goal_addr) const { if (!m_context_valid || goal_addr == 0) { return {}; } const s32 symbol_tag = m_version == GameVersion::Jak1 ? 0 : 1; const s32 offset = s32(goal_addr) - s32(m_debug_context.s7) - symbol_tag; auto kv = m_symbol_offset_to_name_map.find(offset); if (kv != m_symbol_offset_to_name_map.end()) { return kv->second; } return {}; } std::optional Debugger::get_type_name_of_basic(u32 goal_addr) { if (!is_halted() || goal_addr < (u32)BASIC_OFFSET) { return {}; } u32 type_ptr = 0; if (!read_memory_if_safe(&type_ptr, goal_addr - BASIC_OFFSET)) { return {}; } return get_symbol_name_for_value(type_ptr); } std::optional Debugger::get_source_location(u32 goal_addr) { if (!m_context_valid) { return {}; } auto info = get_rip_info(goal_addr + m_debug_context.base); if (!info.knows_function || !info.func_debug) { return {}; } return source_location_for_function_offset(*info.func_debug, info.function_offset); } std::vector Debugger::resolve_source_breakpoint(const std::string& filename, int line, int max_line_slide) { std::vector result; if (!m_reader || !m_listener) { return result; } m_memory_map = m_listener->build_memory_map(); struct Candidate { const FunctionDebugInfo* func = nullptr; std::string func_name; std::string obj_name; int line = -1; int offset = -1; }; std::vector candidates; for (auto& [obj_name, debug_info] : m_debug_info) { for (const auto& [func_name, func] : debug_info.functions()) { Candidate best; for (const auto& instr : func.instructions) { if (instr.kind != InstructionInfo::Kind::IR || instr.offset < 0) { continue; } if (instr.ir_idx < 0 || instr.ir_idx >= int(func.code_sources.size())) { continue; } auto info = m_reader->db.try_get_short_info(func.code_sources.at(instr.ir_idx), false); if (!info || !paths_match(info->filename, filename)) { continue; } const int instr_line = info->line_idx_to_display; // a breakpoint on a blank line or a comment slides forward to the next line with code if (instr_line < line || instr_line > line + max_line_slide) { continue; } if (best.line == -1 || instr_line < best.line || (instr_line == best.line && instr.offset < best.offset)) { best.func = &func; best.func_name = func_name; best.obj_name = func.obj_name.empty() ? obj_name : func.obj_name; best.line = instr_line; best.offset = instr.offset; } } if (best.func) { candidates.push_back(best); } } } if (candidates.empty()) { return result; } int chosen_line = candidates.front().line; for (const auto& c : candidates) { chosen_line = std::min(chosen_line, c.line); } for (const auto& c : candidates) { if (c.line != chosen_line) { continue; } ResolvedBreakpoint bp; bp.line = c.line; bp.function_name = c.func_name; bp.object_name = c.obj_name; listener::MemoryMapEntry entry; if (m_memory_map.lookup(c.obj_name, c.func->seg, &entry)) { bp.goal_addr = entry.start_addr + c.func->offset_in_seg + c.offset; bp.loaded = true; } result.push_back(bp); } return result; } std::vector Debugger::get_live_variables() { std::vector result; if (!(is_valid() && is_attached() && is_halted()) || !m_regs_valid) { return result; } auto info = get_rip_info(get_normalized_rip()); if (!info.knows_function || !info.func_debug || info.func_debug->locals.empty()) { return result; } int current_ir = -1; int best_offset = -1; for (const auto& instr : info.func_debug->instructions) { if (instr.kind != InstructionInfo::Kind::IR || instr.offset < 0) { continue; } if (u32(instr.offset) <= info.function_offset && instr.offset >= best_offset) { best_offset = instr.offset; current_ir = instr.ir_idx; } } if (current_ir < 0) { return result; } const u64 rsp = m_regs_at_break.gprs[emitter::RSP]; for (const auto& local : info.func_debug->locals) { const auto* location = local.location_at(current_ir); if (!location) { continue; } LiveVariable var; var.name = local.name; var.type = local.type; var.is_parameter = local.is_parameter; if (location->kind == VariableLocation::Kind::REGISTER) { var.in_register = true; var.reg = location->reg; } else { const u64 addr = rsp + location->stack_offset; if (addr <= m_debug_context.base) { continue; } var.stack_addr = u32(addr - m_debug_context.base); } result.push_back(var); } return result; } std::vector Debugger::get_source_stack_frames(int max_frames) { std::vector result; if (!(is_valid() && is_attached() && is_halted()) || !m_regs_valid) { return result; } auto bt = get_backtrace(get_normalized_rip(), m_regs_at_break.gprs[emitter::RSP], {}, true); for (size_t i = 0; i < bt.size() && int(result.size()) < max_frames; i++) { const auto& frame = bt.at(i); SourceStackFrame out; out.function_name = frame.rip_info.knows_function ? frame.rip_info.function_name : "(unknown function)"; out.object_name = frame.rip_info.knows_object ? frame.rip_info.object_name : ""; out.rip = frame.rip_info.real_rip; out.goal_rip = frame.rip_info.goal_rip; out.rsp = frame.rsp_at_rip; if (frame.rip_info.func_debug) { u32 offset = frame.rip_info.function_offset; if (i > 0 && offset > 0) { offset--; } out.source = source_location_for_function_offset(*frame.rip_info.func_debug, offset); } result.push_back(out); } return result; } void Debugger::place_breakpoints() { if (!is_halted()) { return; } u8 int3 = 0xcc; for (auto& [addr, bp] : m_addr_breakpoints) { (void)bp; write_memory(&int3, 1, addr); } } void Debugger::remove_breakpoints() { if (!is_halted()) { return; } for (auto& [addr, bp] : m_addr_breakpoints) { write_memory(&bp.old_data, 1, addr); } } bool Debugger::normalize_rip_after_break() { if (!m_regs_valid || !m_context_valid) { return false; } const u64 rip_goal = m_regs_at_break.rip - m_debug_context.base; if (rip_goal == 0) { return true; } if (m_addr_breakpoints.find(u32(rip_goal - 1)) == m_addr_breakpoints.end()) { return true; } m_regs_at_break.rip--; if (!xdbg::set_regs_now(m_debug_context.tid, m_regs_at_break)) { return false; } m_continue_info.valid = false; update_continue_info(); return true; } bool Debugger::single_step_once() { if (!(is_valid() && is_attached() && is_halted())) { return false; } m_continue_info.valid = false; m_regs_valid = false; clear_signal_queue(); if (!xdbg::single_step_now(m_debug_context.tid)) { return false; } m_running = true; auto info = pop_signal(); m_running = false; if (info.kind == xdbg::SignalInfo::DISAPPEARED) { return false; } m_regs_valid = xdbg::get_regs_now(m_debug_context.tid, &m_regs_at_break); return m_regs_valid; } bool Debugger::resume_from_break() { if (!(is_valid() && is_attached() && is_halted())) { return false; } if (!m_regs_valid) { m_regs_valid = xdbg::get_regs_now(m_debug_context.tid, &m_regs_at_break); if (!m_regs_valid) { return false; } } const u64 rip_goal = m_regs_at_break.rip - m_debug_context.base; auto bp_it = m_addr_breakpoints.find(u32(rip_goal)); if (bp_it == m_addr_breakpoints.end() && rip_goal > 0) { bp_it = m_addr_breakpoints.find(u32(rip_goal - 1)); if (bp_it != m_addr_breakpoints.end()) { m_regs_at_break.rip--; if (!xdbg::set_regs_now(m_debug_context.tid, m_regs_at_break)) { return false; } } } if (bp_it != m_addr_breakpoints.end()) { const auto bp = bp_it->second; const bool was_suppressed = m_suppress_stop_reporting; m_suppress_stop_reporting = true; bool ok = write_memory(&bp.old_data, 1, bp.goal_addr) && single_step_once(); if (ok) { u8 int3 = 0xcc; ok = write_memory(&int3, 1, bp.goal_addr); } m_suppress_stop_reporting = was_suppressed; if (!ok) { return false; } } m_continue_info.valid = false; m_regs_valid = false; m_expecting_immeidate_break = false; clear_signal_queue(); if (!xdbg::cont_now(m_debug_context.tid)) { return false; } m_running = true; return true; } bool Debugger::run_to_addr(u32 goal_addr) { if (!is_halted()) { return false; } const bool already_a_user_bp = m_addr_breakpoints.find(goal_addr) != m_addr_breakpoints.end(); u8 saved_byte = 0; if (!already_a_user_bp) { if (!read_memory(&saved_byte, 1, goal_addr)) { return false; } u8 int3 = 0xcc; if (!write_memory(&int3, 1, goal_addr)) { return false; } } bool arrived = false; if (resume_from_break()) { auto info = pop_signal(); m_running = false; if (info.kind != xdbg::SignalInfo::DISAPPEARED) { m_regs_valid = xdbg::get_regs_now(m_debug_context.tid, &m_regs_at_break); if (m_regs_valid) { arrived = u32(m_regs_at_break.rip - m_debug_context.base - 1) == goal_addr; } } } if (is_halted()) { bool rewound = false; if (!already_a_user_bp) { // take our temporary int3 back out write_memory(&saved_byte, 1, goal_addr); if (arrived && m_regs_valid) { // it was never in m_addr_breakpoints, so rewind off it by hand m_regs_at_break.rip--; xdbg::set_regs_now(m_debug_context.tid, m_regs_at_break); rewound = true; } } if (!rewound) { // we may have stopped on one of the user's breakpoints instead of ours normalize_rip_after_break(); } } m_continue_info.valid = false; return arrived; } std::optional Debugger::get_return_address_of_current_frame() { if (!m_regs_valid) { return {}; } auto info = get_rip_info(get_normalized_rip()); if (!info.knows_function || !info.func_debug || !info.func_debug->stack_usage) { return {}; } const u64 rsp_at_call = m_regs_at_break.gprs[emitter::RSP] + *info.func_debug->stack_usage; u64 ret = 0; if (!read_memory_if_safe(&ret, rsp_at_call - m_debug_context.base)) { return {}; } return ret; } bool Debugger::do_step(StepKind kind) { if (!(is_valid() && is_attached() && is_halted())) { return false; } if (!m_regs_valid) { m_regs_valid = xdbg::get_regs_now(m_debug_context.tid, &m_regs_at_break); if (!m_regs_valid) { return false; } } m_memory_map = m_listener->build_memory_map(); const auto start_info = get_rip_info(get_normalized_rip()); const FunctionDebugInfo* start_func = start_info.func_debug; const u64 start_rsp = m_regs_at_break.gprs[emitter::RSP]; int start_line = -1; if (start_func) { auto loc = source_location_for_function_offset(*start_func, start_info.function_offset); if (loc) { start_line = loc->line; } } m_suppress_stop_reporting = true; bool ok = true; if (kind == StepKind::OUT_OF) { auto ret = get_return_address_of_current_frame(); if (ret && *ret > m_debug_context.base) { ok = run_to_addr(u32(*ret - m_debug_context.base)); } else { ok = false; } } else { if (!normalize_rip_after_break()) { m_suppress_stop_reporting = false; return false; } remove_breakpoints(); constexpr int MAX_STEPS = 500000; int steps = 0; while (steps++ < MAX_STEPS) { if (!single_step_once()) { ok = false; break; } const u64 rip = m_regs_at_break.rip; const u64 rsp = m_regs_at_break.gprs[emitter::RSP]; auto info = get_rip_info(rip); const bool in_known_goal_code = info.in_goal_mem && info.knows_function && info.func_debug; if (!in_known_goal_code) { // not in goal code, get return address from the top of the stack and get back to it u64 ret = 0; if (rsp > m_debug_context.base && read_memory_if_safe(&ret, rsp - m_debug_context.base) && ret > m_debug_context.base) { place_breakpoints(); const bool got_back = run_to_addr(u32(ret - m_debug_context.base)); remove_breakpoints(); if (!got_back) { // we stopped for some other reason (breakpoint or crash), that stop wins break; } continue; } // can't work out where we are or how to get back; stop here rather than run away break; } if (info.func_debug != start_func) { if (rsp < start_rsp) { // we've called into something if (kind == StepKind::INTO) { // settle on the first instruction in the callee with source info (the prologue // usually doesn't have any) int settle = 0; while (settle++ < 200) { auto here = get_rip_info(m_regs_at_break.rip); if (here.func_debug && source_location_for_function_offset(*here.func_debug, here.function_offset)) { break; } if (!single_step_once()) { ok = false; break; } } break; } // stepping over, the return address is on top of the stack right after the call u64 ret = 0; if (rsp > m_debug_context.base && read_memory_if_safe(&ret, rsp - m_debug_context.base) && ret > m_debug_context.base) { place_breakpoints(); const bool got_back = run_to_addr(u32(ret - m_debug_context.base)); remove_breakpoints(); if (!got_back) { break; } continue; } break; } // we returned out of the function we started in, that's a completed step break; } // same function, are we on a new source line yet? auto loc = source_location_for_function_offset(*info.func_debug, info.function_offset); if (loc && loc->line != start_line) { break; } } place_breakpoints(); } m_suppress_stop_reporting = false; // refresh what everything else reads after a stop if (ok && is_halted()) { m_regs_valid = xdbg::get_regs_now(m_debug_context.tid, &m_regs_at_break); if (m_regs_valid) { m_break_info = get_rip_info(get_normalized_rip()); } m_continue_info.valid = false; update_continue_info(); } return ok; }