aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-08-29 01:38:42 +0200
committerhachem <im@hachem.wtf>2026-08-29 01:38:42 +0200
commitba64bcc9c38c04bd48e79ac59788b946454f4acb (patch)
tree8e8fb771f82127091e43f2dad334e74d6813d10e
parentab021cb74a2b3f0dab3258bf50929912100d1974 (diff)
feat: disassembly
-rw-r--r--src/debugger/debugger.cpp333
-rw-r--r--src/debugger/debugger.h13
-rw-r--r--src/debugger/types.h31
-rw-r--r--src/ui/ui.cpp350
-rw-r--r--src/ui/ui.h11
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;
};
}