diff options
| author | hachem <im@hachem.wtf> | 2026-08-29 01:38:42 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-08-29 01:38:42 +0200 |
| commit | ba64bcc9c38c04bd48e79ac59788b946454f4acb (patch) | |
| tree | 8e8fb771f82127091e43f2dad334e74d6813d10e | |
| parent | ab021cb74a2b3f0dab3258bf50929912100d1974 (diff) | |
feat: disassembly
| -rw-r--r-- | src/debugger/debugger.cpp | 333 | ||||
| -rw-r--r-- | src/debugger/debugger.h | 13 | ||||
| -rw-r--r-- | src/debugger/types.h | 31 | ||||
| -rw-r--r-- | src/ui/ui.cpp | 350 | ||||
| -rw-r--r-- | src/ui/ui.h | 11 |
5 files changed, 731 insertions, 7 deletions
diff --git a/src/debugger/debugger.cpp b/src/debugger/debugger.cpp index 00bdda7..9bc36da 100644 --- a/src/debugger/debugger.cpp +++ b/src/debugger/debugger.cpp @@ -135,7 +135,9 @@ namespace Hsdbg breakpoint.line = line_entry.GetLine(); - if (breakpoint.by_function) + // only a file and line breakpoint was told where it belongs, the rest + // learn it from wherever lldb landed + if (breakpoint.by_function || breakpoint.by_address) breakpoint.file = path_of(line_entry.GetFileSpec()); } @@ -190,6 +192,10 @@ namespace Hsdbg constexpr uint32_t MAX_VARIABLE_DEPTH = 3; constexpr uint32_t MAX_VARIABLE_CHILDREN = 64; + // only used for frames lldb cannot name, where there is no function to + // bound the listing + constexpr uint32_t DISASSEMBLY_WINDOW = 64; + auto frame_of(lldb::SBProcess& process, uint64_t thread_id, uint32_t frame_index) -> lldb::SBFrame { lldb::SBThread thread = process.GetThreadByID(thread_id); @@ -200,6 +206,114 @@ namespace Hsdbg return thread.GetFrameAtIndex(frame_index); } + auto instruction_of(lldb::SBTarget& target, + lldb::SBInstruction source, + lldb::addr_t program_counter) -> Instruction + { + Instruction entry; + + if (!source.IsValid()) + return entry; + + lldb::SBAddress address = source.GetAddress(); + const lldb::addr_t load = address.GetLoadAddress(target); + + entry.file_address = address.GetFileAddress(); + entry.address = load != LLDB_INVALID_ADDRESS ? load : entry.file_address; + entry.mnemonic = text_or(source.GetMnemonic(target), "?"); + entry.operands = text_or(source.GetOperands(target), ""); + entry.comment = text_or(source.GetComment(target), ""); + entry.size = static_cast<uint32_t>(source.GetByteSize()); + entry.current = program_counter != LLDB_INVALID_ADDRESS && + entry.address == program_counter; + + return entry; + } + + auto instructions_of(lldb::SBTarget& target, + lldb::SBInstructionList source, + lldb::addr_t program_counter) -> std::vector<Instruction> + { + std::vector<Instruction> instructions; + + for (uint32_t index = 0; index < source.GetSize(); ++index) + { + Instruction entry = instruction_of(target, source.GetInstructionAtIndex(index), program_counter); + + if (entry.file_address == 0 && entry.mnemonic.empty()) + continue; + + instructions.push_back(std::move(entry)); + } + + return instructions; + } + + auto module_symbols(lldb::SBTarget& target) -> std::vector<Symbol> + { + std::vector<Symbol> symbols; + + lldb::SBModule module = target.FindModule(target.GetExecutable()); + + if (!module.IsValid()) + return symbols; + + struct Candidate + { + uint64_t file_address = 0; + uint64_t size = 0; + lldb::SBSymbol symbol; + }; + + std::vector<Candidate> candidates; + + for (size_t index = 0; index < module.GetNumSymbols(); ++index) + { + lldb::SBSymbol symbol = module.GetSymbolAtIndex(index); + + if (!symbol.IsValid() || symbol.GetType() != lldb::eSymbolTypeCode) + continue; + + lldb::SBAddress start = symbol.GetStartAddress(); + + if (!start.IsValid()) + continue; + + candidates.push_back({ start.GetFileAddress(), symbol.GetSize(), symbol }); + } + + std::ranges::sort(candidates, [](const Candidate& left, const Candidate& right) + { + if (left.file_address != right.file_address) + return left.file_address < right.file_address; + + return left.size > right.size; + }); + + uint64_t previous = LLDB_INVALID_ADDRESS; + + for (Candidate& candidate : candidates) + { + if (candidate.file_address == previous) + continue; + + previous = candidate.file_address; + + const lldb::addr_t load = candidate.symbol.GetStartAddress().GetLoadAddress(target); + + Symbol entry; + entry.name = text_or(candidate.symbol.GetDisplayName(), + text_or(candidate.symbol.GetName(), "?")); + entry.file_address = candidate.file_address; + entry.address = load != LLDB_INVALID_ADDRESS ? load : candidate.file_address; + entry.size = candidate.size; + + symbols.push_back(std::move(entry)); + } + + return symbols; + } + auto display_of(lldb::SBValue& value) -> std::string { const char* text = value.GetValue(); @@ -341,6 +455,11 @@ namespace Hsdbg for (Breakpoint& breakpoint : m_breakpoints) resolve_breakpoint(breakpoint); + m_disassembly.clear(); + m_disassembly_name.clear(); + m_selected_symbol = 0; + refresh_disassembly(); + return {}; } @@ -360,21 +479,25 @@ namespace Hsdbg m_call_stack.clear(); m_locals.clear(); m_registers.clear(); + m_symbols.clear(); + m_disassembly.clear(); + m_disassembly_name.clear(); m_console_output.clear(); m_selected_thread = 0; m_selected_frame = 0; + m_selected_symbol = 0; for (Breakpoint& breakpoint : m_breakpoints) { breakpoint.backend_id = 0; breakpoint.resolved = false; breakpoint.hit_count = 0; - breakpoint.address = 0; + breakpoint.address = breakpoint.by_address ? breakpoint.file_address : 0; // whatever lldb told us about a named breakpoint belonged to the // binary that just went away - if (breakpoint.by_function) + if (breakpoint.by_function || breakpoint.by_address) { breakpoint.file.clear(); breakpoint.line = 0; @@ -727,6 +850,30 @@ namespace Hsdbg return m_breakpoints.back().id; } + auto Debugger::add_address_breakpoint(uint64_t file_address) -> uint32_t + { + const auto existing = std::ranges::find_if(m_breakpoints, [&](const Breakpoint& candidate) + { + return candidate.by_address && candidate.file_address == file_address; + }); + + if (existing != m_breakpoints.end()) + return existing->id; + + Breakpoint breakpoint; + breakpoint.id = m_next_breakpoint_id++; + breakpoint.by_address = true; + breakpoint.file_address = file_address; + breakpoint.address = file_address; + + m_breakpoints.push_back(std::move(breakpoint)); + resolve_breakpoint(m_breakpoints.back()); + + Log::info("debugger: breakpoint {} at 0x{:x}", m_breakpoints.back().id, file_address); + + return m_breakpoints.back().id; + } + auto Debugger::remove_breakpoint(uint32_t id) -> bool { const auto entry = std::ranges::find(m_breakpoints, id, &Breakpoint::id); @@ -910,6 +1057,16 @@ namespace Hsdbg return true; } + auto Debugger::select_symbol(uint64_t file_address) -> bool + { + if (!m_session->target.IsValid() || file_address == 0) + return false; + + load_disassembly(file_address); + + return !m_disassembly.empty(); + } + auto Debugger::update() -> void { pump_events(); @@ -975,6 +1132,11 @@ namespace Hsdbg case lldb::eStateStepping: m_locals.clear(); m_registers.clear(); + + // the instructions are still the ones on screen, only the + // program counter stopped meaning anything + for (Instruction& instruction : m_disassembly) + instruction.current = false; set_state(TargetState::Running); break; @@ -1129,6 +1291,9 @@ namespace Hsdbg m_selected_thread = 0; m_selected_frame = 0; + // the target outlives the process, so fall back to reading the binary + refresh_disassembly(); + set_state(TargetState::Exited); } @@ -1173,6 +1338,8 @@ namespace Hsdbg m_locals.clear(); m_registers.clear(); + refresh_disassembly(); + if (!is_alive(m_session->process)) return; @@ -1215,6 +1382,155 @@ namespace Hsdbg } } + auto Debugger::refresh_symbols() -> void + { + m_symbols.clear(); + + if (!m_session->target.IsValid()) + return; + + m_symbols = module_symbols(m_session->target); + } + + auto Debugger::load_disassembly(uint64_t file_address) -> void + { + m_disassembly.clear(); + m_disassembly_name.clear(); + m_selected_symbol = 0; + + if (!m_session->target.IsValid() || file_address == 0) + return; + + lldb::SBAddress address = m_session->target.ResolveFileAddress(file_address); + + if (!address.IsValid()) + return; + + lldb::addr_t program_counter = LLDB_INVALID_ADDRESS; + + if (is_alive(m_session->process)) + { + lldb::SBFrame frame = frame_of(m_session->process, m_selected_thread, m_selected_frame); + + if (frame.IsValid()) + program_counter = frame.GetPC(); + } + + lldb::SBSymbolContext context = address.GetSymbolContext(lldb::eSymbolContextEverything); + lldb::SBInstructionList instructions; + std::string name; + + if (lldb::SBFunction function = context.GetFunction(); function.IsValid()) + { + instructions = function.GetInstructions(m_session->target); + name = text_or(function.GetDisplayName(), text_or(function.GetName(), "?")); + } + else if (lldb::SBSymbol symbol = context.GetSymbol(); symbol.IsValid()) + { + instructions = symbol.GetInstructions(m_session->target); + name = text_or(symbol.GetDisplayName(), text_or(symbol.GetName(), "?")); + } + else + { + instructions = m_session->target.ReadInstructions(address, DISASSEMBLY_WINDOW); + name = std::format("{:#x}", file_address); + } + + m_disassembly = instructions_of(m_session->target, instructions, program_counter); + m_disassembly_name = std::move(name); + m_selected_symbol = file_address; + } + + auto Debugger::refresh_disassembly() -> void + { + if (!m_session->target.IsValid()) + { + m_symbols.clear(); + m_disassembly.clear(); + m_disassembly_name.clear(); + m_selected_symbol = 0; + return; + } + + if (m_symbols.empty()) + refresh_symbols(); + + for (Symbol& symbol : m_symbols) + { + lldb::SBAddress address = m_session->target.ResolveFileAddress(symbol.file_address); + const lldb::addr_t load = address.IsValid() + ? address.GetLoadAddress(m_session->target) + : LLDB_INVALID_ADDRESS; + + symbol.address = load != LLDB_INVALID_ADDRESS ? load : symbol.file_address; + } + + lldb::SBFrame frame; + lldb::addr_t program_counter = LLDB_INVALID_ADDRESS; + lldb::addr_t file_pc = LLDB_INVALID_ADDRESS; + + if (is_alive(m_session->process)) + frame = frame_of(m_session->process, m_selected_thread, m_selected_frame); + + if (frame.IsValid()) + { + program_counter = frame.GetPC(); + file_pc = frame.GetPCAddress().GetFileAddress(); + } + + uint64_t wanted = m_selected_symbol; + + if (file_pc != LLDB_INVALID_ADDRESS) + { + const auto containing = std::ranges::find_if(m_symbols, [&](const Symbol& symbol) + { + if (symbol.size == 0) + return symbol.file_address == file_pc; + + return file_pc >= symbol.file_address && + file_pc < symbol.file_address + symbol.size; + }); + + if (containing != m_symbols.end()) + { + wanted = containing->file_address; + } + else if (lldb::SBFunction function = frame.GetFunction(); function.IsValid()) + { + wanted = function.GetStartAddress().GetFileAddress(); + } + else if (lldb::SBSymbol symbol = frame.GetSymbol(); symbol.IsValid()) + { + wanted = symbol.GetStartAddress().GetFileAddress(); + } + } + + if (wanted == 0) + { + const auto main = std::ranges::find(m_symbols, "main", &Symbol::name); + wanted = main != m_symbols.end() ? main->file_address + : (m_symbols.empty() ? 0 : m_symbols.front().file_address); + } + + if (wanted != 0 && (wanted != m_selected_symbol || m_disassembly.empty())) + { + load_disassembly(wanted); + return; + } + + for (Instruction& instruction : m_disassembly) + { + lldb::SBAddress address = m_session->target.ResolveFileAddress(instruction.file_address); + const lldb::addr_t load = address.IsValid() + ? address.GetLoadAddress(m_session->target) + : LLDB_INVALID_ADDRESS; + + instruction.address = load != LLDB_INVALID_ADDRESS ? load : instruction.file_address; + instruction.current = program_counter != LLDB_INVALID_ADDRESS && + instruction.address == program_counter; + } + } + auto Debugger::resolve_breakpoint(Breakpoint& breakpoint) -> void { if (!m_session->target.IsValid()) @@ -1222,7 +1538,16 @@ namespace Hsdbg lldb::SBBreakpoint created; - if (breakpoint.by_function) + if (breakpoint.by_address) + { + // a section relative address keeps pointing at the same instruction + // when the next run loads the binary somewhere else + lldb::SBAddress address = m_session->target.ResolveFileAddress(breakpoint.file_address); + + if (address.IsValid()) + created = m_session->target.BreakpointCreateBySBAddress(address); + } + else if (breakpoint.by_function) { // unscoped, a name like main matches every module the target pulls in created = m_session->target.BreakpointCreateByName( diff --git a/src/debugger/debugger.h b/src/debugger/debugger.h index 82a993a..175683e 100644 --- a/src/debugger/debugger.h +++ b/src/debugger/debugger.h @@ -43,6 +43,7 @@ namespace Hsdbg // breakpoints, the only part that keeps real state for now auto add_breakpoint(const std::filesystem::path& file, uint32_t line) -> uint32_t; auto add_function_breakpoint(std::string_view function) -> uint32_t; + auto add_address_breakpoint(uint64_t file_address) -> uint32_t; auto remove_breakpoint(uint32_t id) -> bool; auto set_breakpoint_enabled(uint32_t id, bool enabled) -> bool; auto set_breakpoint_condition(uint32_t id, std::string_view condition) -> bool; @@ -56,6 +57,9 @@ namespace Hsdbg auto call_stack() const -> std::span<const StackFrame> { return m_call_stack; } auto locals() const -> std::span<const Variable> { return m_locals; } auto registers() const -> std::span<const Register> { return m_registers; } + auto symbols() const -> std::span<const Symbol> { return m_symbols; } + auto disassembly() const -> std::span<const Instruction> { return m_disassembly; } + auto disassembly_name() const -> std::string_view { return m_disassembly_name; } auto evaluate(std::string_view expression) -> Result<std::string>; auto read_memory(uint64_t address, size_t size) -> Result<std::vector<uint8_t>>; auto console_output() const -> std::span<const std::string> { return m_console_output; } @@ -63,8 +67,10 @@ namespace Hsdbg // selection, what the ui is currently looking at auto select_thread(uint64_t thread_id) -> bool; auto select_frame(uint32_t frame_index) -> bool; + auto select_symbol(uint64_t file_address) -> bool; auto selected_thread() const -> uint64_t { return m_selected_thread; } auto selected_frame() const -> uint32_t { return m_selected_frame; } + auto selected_symbol() const -> uint64_t { return m_selected_symbol; } // pumps whatever the debug session has to say, called once per frame auto update() -> void; @@ -99,6 +105,9 @@ namespace Hsdbg auto on_exited() -> void; auto refresh_call_stack() -> void; auto refresh_frame_data() -> void; + auto refresh_symbols() -> void; + auto refresh_disassembly() -> void; + auto load_disassembly(uint64_t file_address) -> void; std::unique_ptr<Session> m_session; @@ -115,10 +124,14 @@ namespace Hsdbg std::vector<StackFrame> m_call_stack; std::vector<Variable> m_locals; std::vector<Register> m_registers; + std::vector<Symbol> m_symbols; + std::vector<Instruction> m_disassembly; + std::string m_disassembly_name; std::vector<std::string> m_console_output; uint64_t m_selected_thread = 0; uint32_t m_selected_frame = 0; + uint64_t m_selected_symbol = 0; uint64_t m_stop_count = 0; }; } diff --git a/src/debugger/types.h b/src/debugger/types.h index dec74e5..e288f85 100644 --- a/src/debugger/types.h +++ b/src/debugger/types.h @@ -96,6 +96,11 @@ namespace Hsdbg // set by name, so file and line are whatever lldb found rather than // what was asked for bool by_function = false; + + // set on an instruction, identified by where it sits in the binary so it + // survives the process being restarted somewhere else + bool by_address = false; + uint64_t file_address = 0; }; struct StackFrame @@ -127,4 +132,30 @@ namespace Hsdbg std::string name; uint64_t value = 0; }; + + struct Instruction + { + // where the instruction is right now, which is the load address once a + // process is running and the address in the binary before that + uint64_t address = 0; + + // the same instruction across runs, so a breakpoint can point at it + uint64_t file_address = 0; + + std::string mnemonic; + std::string operands; + std::string comment; + uint32_t size = 0; + + // whether the selected frame is sitting on this one + bool current = false; + }; + + struct Symbol + { + std::string name; + uint64_t file_address = 0; + uint64_t address = 0; + uint64_t size = 0; + }; } diff --git a/src/ui/ui.cpp b/src/ui/ui.cpp index 01c5165..b79da17 100644 --- a/src/ui/ui.cpp +++ b/src/ui/ui.cpp @@ -12,9 +12,11 @@ #include <backends/imgui_impl_opengl3.h> #include <algorithm> +#include <cctype> #include <filesystem> #include <format> #include <span> +#include <vector> namespace Hsdbg { @@ -26,6 +28,8 @@ namespace Hsdbg constexpr const char* PANEL_THREADS = "threads"; constexpr const char* PANEL_LOCALS = "locals"; constexpr const char* PANEL_REGISTERS = "registers"; + constexpr const char* PANEL_SYMBOLS = "symbols"; + constexpr const char* PANEL_DISASSEMBLY = "disassembly"; constexpr const char* PANEL_CONSOLE = "console"; constexpr const char* LOAD_TARGET_POPUP = "load target"; @@ -40,6 +44,169 @@ namespace Hsdbg const ImVec2 ROOT_PADDING(8.0f, 6.0f); const ImVec2 TOOLBAR_PADDING(4.0f, 4.0f); + // the same green the source view puts behind the current line + const ImU32 CURRENT_INSTRUCTION_COLOR = IM_COL32(58, 72, 46, 255); + + constexpr float BREAKPOINT_RADIUS = 5.0f; + constexpr float DISASSEMBLY_GUTTER_WIDTH = 22.0f; + + const ImU32 BREAKPOINT_COLOR = IM_COL32(226, 84, 84, 255); + const ImU32 BREAKPOINT_DISABLED_COLOR = IM_COL32(120, 90, 90, 255); + const ImU32 BREAKPOINT_HOVER_COLOR = IM_COL32(226, 84, 84, 90); + + auto breakpoint_at(Debugger& debugger, const Instruction& instruction) -> const Breakpoint* + { + for (const Breakpoint& candidate : debugger.breakpoints()) + { + if (instruction.file_address != 0 && + candidate.file_address == instruction.file_address) + { + return &candidate; + } + + if (candidate.address != 0 && candidate.address == instruction.address) + return &candidate; + } + + return nullptr; + } + + auto toggle_instruction_breakpoint(Debugger& debugger, const Instruction& instruction) -> void + { + if (const Breakpoint* existing = breakpoint_at(debugger, instruction); existing != nullptr) + { + debugger.remove_breakpoint(existing->id); + return; + } + + if (instruction.file_address != 0) + debugger.add_address_breakpoint(instruction.file_address); + } + + auto matches_filter(std::string_view name, std::string_view filter) -> bool + { + if (filter.empty()) + return true; + + if (filter.size() > name.size()) + return false; + + const auto equal = [](char left, char right) + { + return std::tolower(static_cast<unsigned char>(left)) == + std::tolower(static_cast<unsigned char>(right)); + }; + + return std::ranges::search(name, filter, equal).begin() != name.end(); + } + + auto draw_instruction_table(Debugger& debugger, + std::span<const Instruction> instructions, + bool scroll_to_current) -> void + { + constexpr ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | + ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY | + ImGuiTableFlags_NoPadOuterX; + + if (!ImGui::BeginTable("##instructions", 5, flags, ImVec2(0.0f, 0.0f))) + return; + + ImGui::TableSetupColumn("##gutter", + ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoResize, + DISASSEMBLY_GUTTER_WIDTH); + ImGui::TableSetupColumn("address", ImGuiTableColumnFlags_WidthFixed, 120.0f); + ImGui::TableSetupColumn("mnemonic", ImGuiTableColumnFlags_WidthFixed, 70.0f); + ImGui::TableSetupColumn("operands"); + ImGui::TableSetupColumn("comment"); + ImGui::TableHeadersRow(); + + const float row_height = ImGui::GetTextLineHeightWithSpacing(); + + if (scroll_to_current) + { + int scroll_index = 0; + + for (int index = 0; index < static_cast<int>(instructions.size()); ++index) + { + if (instructions[static_cast<size_t>(index)].current) + { + scroll_index = index; + break; + } + } + + ImGui::SetScrollY(static_cast<float>(scroll_index) * row_height - + ImGui::GetWindowHeight() * 0.35f); + } + + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + ImGuiListClipper clipper; + clipper.Begin(static_cast<int>(instructions.size()), row_height); + + while (clipper.Step()) + { + for (int index = clipper.DisplayStart; index < clipper.DisplayEnd; ++index) + { + const Instruction& instruction = instructions[static_cast<size_t>(index)]; + const Breakpoint* breakpoint = breakpoint_at(debugger, instruction); + + ImGui::TableNextRow(); + ImGui::PushID(index); + + if (instruction.current) + { + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, CURRENT_INSTRUCTION_COLOR); + } + + ImGui::TableNextColumn(); + + const ImVec2 gutter_min = ImGui::GetCursorScreenPos(); + + if (ImGui::InvisibleButton("##gutter", ImVec2(DISASSEMBLY_GUTTER_WIDTH, row_height))) + toggle_instruction_breakpoint(debugger, instruction); + + const bool gutter_hovered = ImGui::IsItemHovered(); + const ImVec2 marker_center(gutter_min.x + DISASSEMBLY_GUTTER_WIDTH * 0.5f, + gutter_min.y + row_height * 0.5f); + + if (breakpoint != nullptr) + { + const ImU32 color = breakpoint->enabled ? BREAKPOINT_COLOR + : BREAKPOINT_DISABLED_COLOR; + + if (breakpoint->resolved) + draw_list->AddCircleFilled(marker_center, BREAKPOINT_RADIUS, color); + else + draw_list->AddCircle(marker_center, BREAKPOINT_RADIUS, color, 0, 1.5f); + } + else if (gutter_hovered) + { + draw_list->AddCircleFilled(marker_center, BREAKPOINT_RADIUS, BREAKPOINT_HOVER_COLOR); + } + + ImGui::TableNextColumn(); + ImGui::Text("0x%012llx", static_cast<unsigned long long>(instruction.address)); + + if (scroll_to_current && instruction.current) + ImGui::SetScrollHereY(0.35f); + + ImGui::TableNextColumn(); + ImGui::TextUnformatted(instruction.mnemonic.c_str()); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(instruction.operands.c_str()); + ImGui::TableNextColumn(); + + if (!instruction.comment.empty()) + ImGui::TextDisabled("; %s", instruction.comment.c_str()); + + ImGui::PopID(); + } + } + + ImGui::EndTable(); + } + auto state_color(TargetState state) -> ImVec4 { switch (state) @@ -154,6 +321,10 @@ namespace Hsdbg m_followed_stop = debugger.stop_count(); + // wanted even when no frame has source to show + m_scroll_to_program_counter = true; + m_scroll_to_symbol = true; + const std::span<const StackFrame> stack = debugger.call_stack(); // the innermost frames are often runtime internals with no source, and @@ -170,6 +341,23 @@ namespace Hsdbg show_frame(*frame); } + auto Ui::follow_target(Debugger& debugger) -> void + { + if (!debugger.has_target()) + { + m_followed_target.clear(); + return; + } + + if (debugger.target_path() == m_followed_target) + return; + + m_followed_target = debugger.target_path(); + m_focus_symbols = true; + m_scroll_to_symbol = true; + m_scroll_to_program_counter = true; + } + auto Ui::show_frame(const StackFrame& frame) -> void { if (frame.file.empty() || frame.line == 0) @@ -206,6 +394,7 @@ namespace Hsdbg draw_toolbar(debugger); follow_stop(debugger); + follow_target(debugger); const ImGuiID dockspace_id = ImGui::GetID("##hsdbg_dockspace"); const float status_bar_height = ImGui::GetTextLineHeight() + @@ -235,6 +424,8 @@ namespace Hsdbg draw_threads_panel(debugger); draw_locals_panel(debugger); draw_registers_panel(debugger); + draw_symbols_panel(debugger); + draw_disassembly_panel(debugger); draw_console_panel(debugger); if (m_visible.demo) @@ -242,7 +433,19 @@ namespace Hsdbg // a window claims its tab when it is first submitted, so this can only be // asked for once every panel in the node exists - if (m_focus_breakpoints) + if (m_focus_symbols) + { + ImGui::SetWindowFocus(PANEL_SYMBOLS); + m_focus_symbols = false; + m_focus_breakpoints = false; + } + else if (m_focus_disassembly) + { + ImGui::SetWindowFocus(PANEL_DISASSEMBLY); + m_focus_disassembly = false; + m_focus_breakpoints = false; + } + else if (m_focus_breakpoints) { ImGui::SetWindowFocus(PANEL_BREAKPOINTS); m_focus_breakpoints = false; @@ -267,7 +470,9 @@ namespace Hsdbg const ImGuiID right_bottom_id = ImGui::DockBuilderSplitNode(right_top_id, ImGuiDir_Down, 0.5f, nullptr, &right_top_id); ImGui::DockBuilderDockWindow(PANEL_SOURCE, center_id); + ImGui::DockBuilderDockWindow(PANEL_DISASSEMBLY, center_id); ImGui::DockBuilderDockWindow(PANEL_THREADS, left_top_id); + ImGui::DockBuilderDockWindow(PANEL_SYMBOLS, left_bottom_id); ImGui::DockBuilderDockWindow(PANEL_CALL_STACK, left_bottom_id); ImGui::DockBuilderDockWindow(PANEL_LOCALS, right_top_id); ImGui::DockBuilderDockWindow(PANEL_REGISTERS, right_bottom_id); @@ -343,6 +548,8 @@ namespace Hsdbg ImGui::MenuItem(PANEL_THREADS, nullptr, &m_visible.threads); ImGui::MenuItem(PANEL_LOCALS, nullptr, &m_visible.locals); ImGui::MenuItem(PANEL_REGISTERS, nullptr, &m_visible.registers); + ImGui::MenuItem(PANEL_SYMBOLS, nullptr, &m_visible.symbols); + ImGui::MenuItem(PANEL_DISASSEMBLY, nullptr, &m_visible.disassembly); ImGui::MenuItem(PANEL_CONSOLE, nullptr, &m_visible.console); ImGui::Separator(); @@ -515,7 +722,7 @@ namespace Hsdbg { if (debugger.breakpoints().empty()) { - ImGui::TextDisabled("no breakpoints, click a line gutter in the source panel"); + ImGui::TextDisabled("no breakpoints, click a gutter in source or disassembly"); } else { @@ -552,7 +759,12 @@ namespace Hsdbg ImGui::Text("%u", breakpoint.id); ImGui::TableNextColumn(); - if (breakpoint.line != 0) + if (breakpoint.by_address) + { + ImGui::Text("0x%llx", + static_cast<unsigned long long>(breakpoint.file_address)); + } + else if (breakpoint.line != 0) { ImGui::Text("%s:%u", breakpoint.file.filename().string().c_str(), @@ -647,6 +859,9 @@ namespace Hsdbg { debugger.select_frame(frame.index); show_frame(frame); + + m_scroll_to_program_counter = true; + m_scroll_to_symbol = true; } if (frame.line == 0) @@ -680,8 +895,12 @@ namespace Hsdbg const std::string label = std::format("{} {}", thread.id, thread.name); if (ImGui::Selectable(label.c_str(), selected)) + { debugger.select_thread(thread.id); + m_scroll_to_program_counter = true; + } + ImGui::SameLine(); ImGui::TextDisabled("%s", to_string(thread.stop_reason).data()); } @@ -766,6 +985,131 @@ namespace Hsdbg ImGui::End(); } + auto Ui::draw_symbols_panel(Debugger& debugger) -> void + { + if (!m_visible.symbols) + return; + + if (ImGui::Begin(PANEL_SYMBOLS, &m_visible.symbols)) + { + const std::span<const Symbol> symbols = debugger.symbols(); + + if (symbols.empty()) + { + ImGui::TextDisabled("no symbols"); + } + else + { + ImGui::SetNextItemWidth(-1.0f); + ImGui::InputTextWithHint("##symbol_filter", "filter symbols", &m_symbol_filter); + + std::vector<size_t> visible; + + for (size_t index = 0; index < symbols.size(); ++index) + { + if (matches_filter(symbols[index].name, m_symbol_filter)) + visible.push_back(index); + } + + ImGui::TextDisabled("%zu / %zu", visible.size(), symbols.size()); + + constexpr ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | + ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY | + ImGuiTableFlags_NoPadOuterX; + + if (ImGui::BeginTable("##symbols", 2, flags)) + { + ImGui::TableSetupColumn("address", ImGuiTableColumnFlags_WidthFixed, 110.0f); + ImGui::TableSetupColumn("name"); + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableHeadersRow(); + + const float row_height = ImGui::GetTextLineHeightWithSpacing(); + + if (m_scroll_to_symbol) + { + for (int index = 0; index < static_cast<int>(visible.size()); ++index) + { + if (symbols[visible[static_cast<size_t>(index)]].file_address == + debugger.selected_symbol()) + { + ImGui::SetScrollY(static_cast<float>(index) * row_height - + ImGui::GetWindowHeight() * 0.35f); + break; + } + } + + m_scroll_to_symbol = false; + } + + ImGuiListClipper clipper; + clipper.Begin(static_cast<int>(visible.size()), row_height); + + while (clipper.Step()) + { + for (int index = clipper.DisplayStart; index < clipper.DisplayEnd; ++index) + { + const Symbol& symbol = symbols[visible[static_cast<size_t>(index)]]; + const bool selected = symbol.file_address == debugger.selected_symbol(); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + + ImGui::PushID(static_cast<int>(symbol.file_address)); + + if (ImGui::Selectable(std::format("0x{:012x}", symbol.address).c_str(), + selected, + ImGuiSelectableFlags_SpanAllColumns)) + { + debugger.select_symbol(symbol.file_address); + m_focus_disassembly = true; + m_scroll_to_program_counter = true; + } + + if (ImGui::IsItemHovered() && symbol.name.size() > 40) + ImGui::SetTooltip("%s", symbol.name.c_str()); + + ImGui::TableNextColumn(); + ImGui::TextUnformatted(symbol.name.c_str()); + + ImGui::PopID(); + } + } + + ImGui::EndTable(); + } + } + } + + ImGui::End(); + } + + auto Ui::draw_disassembly_panel(Debugger& debugger) -> void + { + if (!m_visible.disassembly) + return; + + if (ImGui::Begin(PANEL_DISASSEMBLY, &m_visible.disassembly)) + { + const std::span<const Instruction> instructions = debugger.disassembly(); + + if (instructions.empty()) + { + ImGui::TextDisabled("no disassembly"); + } + else + { + if (!debugger.disassembly_name().empty()) + ImGui::TextUnformatted(debugger.disassembly_name().data()); + + draw_instruction_table(debugger, instructions, m_scroll_to_program_counter); + m_scroll_to_program_counter = false; + } + } + + ImGui::End(); + } + auto Ui::draw_console_panel(Debugger& debugger) -> void { if (!m_visible.console) diff --git a/src/ui/ui.h b/src/ui/ui.h index df6f7bd..39493dc 100644 --- a/src/ui/ui.h +++ b/src/ui/ui.h @@ -42,6 +42,8 @@ namespace Hsdbg bool threads = true; bool locals = true; bool registers = true; + bool symbols = true; + bool disassembly = true; bool console = true; bool demo = false; }; @@ -60,12 +62,15 @@ namespace Hsdbg auto draw_threads_panel(Debugger& debugger) -> void; auto draw_locals_panel(Debugger& debugger) -> void; auto draw_registers_panel(Debugger& debugger) -> void; + auto draw_symbols_panel(Debugger& debugger) -> void; + auto draw_disassembly_panel(Debugger& debugger) -> void; auto draw_console_panel(Debugger& debugger) -> void; auto push_console(std::string line) -> void; auto report(const Result<void>& result, std::string_view action) -> void; auto follow_stop(Debugger& debugger) -> void; + auto follow_target(Debugger& debugger) -> void; auto show_frame(const StackFrame& frame) -> void; Window* m_window = nullptr; @@ -75,12 +80,18 @@ namespace Hsdbg std::vector<std::string> m_console_lines; std::string m_console_input; std::string m_target_input; + std::string m_symbol_filter; uint64_t m_followed_stop = 0; + std::filesystem::path m_followed_target; bool m_layout_built = false; bool m_console_scroll_pending = false; bool m_load_target_pending = false; bool m_focus_breakpoints = false; + bool m_focus_symbols = false; + bool m_focus_disassembly = false; + bool m_scroll_to_program_counter = false; + bool m_scroll_to_symbol = false; }; } |
