aboutsummaryrefslogtreecommitdiff
path: root/src/ui
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 /src/ui
parentab021cb74a2b3f0dab3258bf50929912100d1974 (diff)
feat: disassembly
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/ui.cpp350
-rw-r--r--src/ui/ui.h11
2 files changed, 358 insertions, 3 deletions
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;
};
}