diff --git a/goalc/compiler/CodeGenerator.cpp b/goalc/compiler/CodeGenerator.cpp index 0a618dce0a..c36ff35962 100644 --- a/goalc/compiler/CodeGenerator.cpp +++ b/goalc/compiler/CodeGenerator.cpp @@ -63,6 +63,7 @@ void CodeGenerator::do_function(FunctionEnv* env, int f_idx) { */ void CodeGenerator::do_goal_function(FunctionEnv* env, int f_idx) { bool use_new_xmms = true; + auto* debug = &m_debug_info->function_by_name(env->name()); auto f_rec = m_gen.get_existing_function_record(f_idx); // todo, extra alignment settings @@ -154,6 +155,7 @@ void CodeGenerator::do_goal_function(FunctionEnv* env, int f_idx) { InstructionInfo::Kind::PROLOGUE); } } + debug->stack_usage = stack_offset; // emit each IR into x86 instructions. for (int ir_idx = 0; ir_idx < int(env->code().size()); ir_idx++) { diff --git a/goalc/compiler/compilation/CompilerControl.cpp b/goalc/compiler/compilation/CompilerControl.cpp index 5b3b9c73df..c5cbac0776 100644 --- a/goalc/compiler/compilation/CompilerControl.cpp +++ b/goalc/compiler/compilation/CompilerControl.cpp @@ -170,7 +170,7 @@ Val* Compiler::compile_asm_file(const goos::Object& form, const goos::Object& re // send to target if (load) { if (m_listener.is_connected()) { - m_listener.send_code(data); + m_listener.send_code(data, obj_file_name); } else { printf("WARNING - couldn't load because listener isn't connected\n"); // todo log warn } diff --git a/goalc/debugger/DebugInfo.h b/goalc/debugger/DebugInfo.h index 1ff97d83de..e10ced32a6 100644 --- a/goalc/debugger/DebugInfo.h +++ b/goalc/debugger/DebugInfo.h @@ -2,6 +2,7 @@ #include #include +#include #include #include "common/util/assert.h" #include "common/common_types.h" @@ -24,6 +25,7 @@ struct FunctionDebugInfo { // the actual bytes in the object file. std::vector generated_code; + std::optional stack_usage; std::string disassemble_debug_info(bool* had_failure); }; @@ -54,6 +56,8 @@ class DebugInfo { return false; } + FunctionDebugInfo& function_by_name(const std::string& name) { return m_functions.at(name); } + void clear() { m_functions.clear(); } std::string disassemble_all_functions(bool* had_failure); diff --git a/goalc/debugger/Debugger.cpp b/goalc/debugger/Debugger.cpp index e81318b9de..ffa1c982ce 100644 --- a/goalc/debugger/Debugger.cpp +++ b/goalc/debugger/Debugger.cpp @@ -4,6 +4,7 @@ * Uses xdbg functions to debug an OpenGOAL target. */ +#include "goalc/emitter/Register.h" #include "common/util/assert.h" #include "Debugger.h" #include "common/util/Timer.h" @@ -147,6 +148,180 @@ std::string Debugger::get_info_about_addr(u32 addr) { } } +/*! + * 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) { + fmt::print("Backtrace:\n"); + std::vector bt; + + if (rip == m_debug_context.base) { + // we jumped to NULL. + fmt::print("Jumped to GOAL 0x0. Attempting to find previous function.\n"); + u64 next_rip = 0; + if (!read_memory_if_safe(&next_rip, rsp - m_debug_context.base)) { + fmt::print(" failed to read return address off of the stack\n"); + return {}; + } + + rip = next_rip; + rsp += 8; + } + + while (true) { + fmt::print(" rsp: 0x{:x} rip: 0x{:x}\n", rsp, rip); + 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) { + fmt::print("{}\n", frame.rip_info.function_name); + // we're good! + 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)) { + fmt::print("Invalid return address encountered!\n"); + break; + } + + rip = next_rip; + rsp = rsp_at_call + 8; // 8 for the call itself. + + } else { + if (!frame.rip_info.knows_function) { + fmt::print("Unknown Function at 0x{:x}\n", rip); + break; + } + if (!frame.rip_info.func_debug) { + fmt::print("Function {} has no debug info.\n", frame.rip_info.function_name); + break; + } else { + fmt::print("Function {} with no stack frame data.\n", frame.rip_info.function_name); + } + break; + } + + bt.push_back(frame); + } + + 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_debug_context.base + info.map_entry->start_addr + func_info->offset_in_seg, + rip + rip_offset, func_info->instructions, func_info->irs, &result.failed); + } + } 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. @@ -166,85 +341,12 @@ void Debugger::update_break_info() { } if (regs_valid()) { - std::vector mem; - mem.resize(INSTR_DUMP_SIZE_REV + INSTR_DUMP_SIZE_FWD); - // very basic asm dump. - auto rip = m_regs_at_break.rip; - m_break_info.real_rip = rip; - m_break_info.goal_rip = rip - m_debug_context.base; + m_break_info = get_rip_info(m_regs_at_break.rip); + update_continue_info(); + auto dis = disassemble_at_rip(m_break_info); + fmt::print("{}\n", dis.text); - m_break_info.disassembly_failed = false; - - if (rip >= m_debug_context.base + EE_MAIN_MEM_LOW_PROTECT && - rip < m_debug_context.base + EE_MAIN_MEM_SIZE) { - read_memory(mem.data(), INSTR_DUMP_SIZE_REV + INSTR_DUMP_SIZE_FWD, - rip - m_debug_context.base - INSTR_DUMP_SIZE_REV); - auto map_loc = m_memory_map.lookup(rip - m_debug_context.base); - if (map_loc.empty) { - fmt::print("In unknown code\n"); - fmt::print("{}", disassemble_x86(mem.data(), mem.size(), rip - INSTR_DUMP_SIZE_REV, rip)); - m_break_info.disassembly_failed = true; - m_break_info.knows_object = false; - m_break_info.knows_function = false; - } else { - u64 obj_offset = rip - m_debug_context.base - map_loc.start_addr; - m_break_info.knows_object = true; - m_break_info.object_name = map_loc.obj_name; - m_break_info.object_seg = map_loc.seg_id; - m_break_info.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)) { - update_continue_info(); - m_break_info.knows_function = true; - m_break_info.function_name = name; - m_break_info.function_offset = obj_offset - info->offset_in_seg; - - assert(!info->instructions.empty()); - - std::vector function_mem; - function_mem.resize(info->instructions.back().offset + - info->instructions.back().instruction.length()); - read_memory(function_mem.data(), function_mem.size(), - map_loc.start_addr + info->offset_in_seg); - - int rip_offset = 0; - if (m_continue_info.valid && m_continue_info.is_addr_breakpiont) { - int offset_in_fmem = uint64_t(m_continue_info.addr_breakpoint.goal_addr) - - uint64_t(map_loc.start_addr + info->offset_in_seg); - if (offset_in_fmem < 0 || offset_in_fmem >= int(function_mem.size())) { - m_break_info.disassembly_failed = true; - } else { - function_mem.at(offset_in_fmem) = m_continue_info.addr_breakpoint.old_data; - rip_offset = -1; - } - } - - fmt::print( - "In function {} in segment {} of obj {}, offset_obj 0x{:x}, offset_func 0x{:x}\n", - name, map_loc.seg_id, map_loc.obj_name, obj_offset, m_break_info.function_offset); - - fmt::print("{}", disassemble_x86_function( - function_mem.data(), function_mem.size(), - m_debug_context.base + map_loc.start_addr + info->offset_in_seg, - rip + rip_offset, info->instructions, info->irs, - &m_break_info.disassembly_failed)); - - } else { - m_break_info.disassembly_failed = true; - m_break_info.knows_function = false; - fmt::print("In segment {} of obj {}, offset 0x{:x}\n", map_loc.seg_id, map_loc.obj_name, - obj_offset); - fmt::print("{}", disassemble_x86(mem.data(), mem.size(), rip - INSTR_DUMP_SIZE_REV, rip)); - } - } - } else { - m_break_info.disassembly_failed = true; - fmt::print("Not in GOAL code!\n"); - } + get_backtrace(m_regs_at_break.rip, m_regs_at_break.gprs[emitter::RSP]); } } @@ -302,11 +404,19 @@ bool Debugger::do_continue() { /*! * Read memory from an attached and halted target. */ -bool Debugger::read_memory(u8* dest_buffer, int size, u32 goal_addr) { +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. */ @@ -618,6 +728,22 @@ void Debugger::update_continue_info() { 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()) { diff --git a/goalc/debugger/Debugger.h b/goalc/debugger/Debugger.h index eed8755673..50a7736e2d 100644 --- a/goalc/debugger/Debugger.h +++ b/goalc/debugger/Debugger.h @@ -15,14 +15,22 @@ #include "common/cross_os_debug/xdbg.h" #include "goalc/listener/MemoryMap.h" #include "DebugInfo.h" +#include namespace listener { class Listener; } -struct BreakInfo { - u64 real_rip = 0; - u32 goal_rip = 0; +/*! + * Information about an instruction pointer, used for constructing a useful disassembly around it. + */ +struct InstructionPointerInfo { + u64 real_rip = 0; // x86-64 rip register value (64-bits) + u32 goal_rip = 0; // GOAL pointer of rip. + + u64 real_rsp = 0; + + bool in_goal_mem = false; bool knows_object = false; std::string object_name; @@ -33,7 +41,19 @@ struct BreakInfo { std::string function_name; u32 function_offset = -1; - bool disassembly_failed = false; + std::optional map_entry; + + FunctionDebugInfo* func_debug = nullptr; +}; + +struct Disassembly { + std::string text; + bool failed = false; +}; + +struct BacktraceFrame { + InstructionPointerInfo rip_info; + u64 rsp_at_rip = 0; }; class Debugger { @@ -51,7 +71,17 @@ class Debugger { bool attach_and_break(); bool do_break(); bool do_continue(); - bool read_memory(u8* dest_buffer, int size, u32 goal_addr); + bool read_memory(u8* dest_buffer, int size, u32 goal_addr) const; + bool read_memory_if_safe(u8* dest_buffer, int size, u32 goal_addr) const; + template + bool read_memory_if_safe(T* dst, u32 goal_addr) const { + u8 temp[sizeof(T)]; + if (read_memory_if_safe(temp, sizeof(T), goal_addr)) { + memcpy(dst, temp, sizeof(T)); + return true; + } + return false; + } bool write_memory(const u8* src_buffer, int size, u32 goal_addr); void read_symbol_table(); u32 get_symbol_address(const std::string& sym_name); @@ -59,9 +89,14 @@ class Debugger { void add_addr_breakpoint(u32 addr); void remove_addr_breakpoint(u32 addr); void update_break_info(); + + InstructionPointerInfo get_rip_info(u64 x86_rip); DebugInfo& get_debug_info_for_object(const std::string& object_name); - const BreakInfo& get_cached_break_info() { return m_break_info; } + const InstructionPointerInfo& get_cached_break_info() { return m_break_info; } std::string get_info_about_addr(u32 addr); + Disassembly disassemble_at_rip(const InstructionPointerInfo& info); + + std::vector get_backtrace(u64 rip, u64 rsp); /*! * Get the x86 address of GOAL memory @@ -150,6 +185,8 @@ class Debugger { Breakpoint addr_breakpoint; } m_continue_info; + ContinueInfo get_continue_info(u64 rip) const; + // for more complicated breakpoint stuff, we have a queue of stops. // right now it's barely used for anything other than waiting for a "break" to be acknowledged. struct SignalInfo { @@ -165,7 +202,7 @@ class Debugger { bool m_running = true; bool m_attached = false; - BreakInfo m_break_info; + InstructionPointerInfo m_break_info; listener::Listener* m_listener = nullptr; listener::MemoryMap m_memory_map; diff --git a/goalc/listener/Listener.cpp b/goalc/listener/Listener.cpp index 095c9f0c2b..461beaefdd 100644 --- a/goalc/listener/Listener.cpp +++ b/goalc/listener/Listener.cpp @@ -337,8 +337,11 @@ int Listener::get_received_message_count() { /*! * Send a "CODE" message for the target to execute as the Listener Function. * Returns once the target acks the code. + * + * The load name is not actually sent to the target. Instead, if the target loads successfully + * and outputs a *listener* load message, this will be remapped to a load of the given name. */ -void Listener::send_code(std::vector& code) { +void Listener::send_code(std::vector& code, const std::optional& load_name) { got_ack = false; int total_size = code.size() + sizeof(ListenerMessageHeader); if (total_size > BUFFER_SIZE) { @@ -346,6 +349,10 @@ void Listener::send_code(std::vector& code) { return; } + rcv_mtx.lock(); + m_pending_listener_load_object_name = load_name; + rcv_mtx.unlock(); + auto* header = (ListenerMessageHeader*)m_buffer; auto* buffer_data = (char*)(header + 1); header->deci2_header.rsvd = 0; @@ -551,7 +558,14 @@ void Listener::handle_output_message(const char* msg) { * Add a load to the load listing. */ void Listener::add_load(const std::string& name, const LoadEntry& le) { - m_load_entries[name] = le; + // if we load a file through the listener, the compiler will set the pending load name, + // and the runtime will send a load message with *listener*. + if (name == "*listener*" && m_pending_listener_load_object_name) { + m_load_entries[*m_pending_listener_load_object_name] = le; + m_pending_listener_load_object_name = {}; + } else { + m_load_entries[name] = le; + } } /*! diff --git a/goalc/listener/Listener.h b/goalc/listener/Listener.h index 156df1a36e..7865fe1a6c 100644 --- a/goalc/listener/Listener.h +++ b/goalc/listener/Listener.h @@ -36,7 +36,7 @@ class Listener { void send_reset(bool shutdown); void send_poke(); void disconnect(); - void send_code(std::vector& code); + void send_code(std::vector& code, const std::optional& load_name = {}); void add_debugger(Debugger* debugger); bool most_recent_send_was_acked() const { return got_ack; } MemoryMap build_memory_map(); @@ -64,6 +64,8 @@ class Listener { ListenerMessageKind filter = ListenerMessageKind::MSG_INVALID; std::vector message_record; std::unordered_map m_load_entries; + + std::optional m_pending_listener_load_object_name; char ack_recv_buff[512]; uint64_t last_sent_id = 0; uint64_t last_recvd_id = 0; diff --git a/test/goalc/test_debugger.cpp b/test/goalc/test_debugger.cpp index 46524d1df6..d0888cd6ec 100644 --- a/test/goalc/test_debugger.cpp +++ b/test/goalc/test_debugger.cpp @@ -182,7 +182,8 @@ TEST(Debugger, SimpleBreakpoint) { EXPECT_TRUE(bi.knows_object); EXPECT_TRUE(bi.object_name == "*listener*"); EXPECT_TRUE(bi.function_name == "test-function"); - EXPECT_FALSE(bi.disassembly_failed); + auto disasm = compiler.get_debugger().disassemble_at_rip(bi); + EXPECT_FALSE(disasm.failed); // if we change this to be before the break instruction this might need to be 0 in the future. EXPECT_EQ(bi.function_offset, 1);