aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-08-27 14:17:34 +0200
committerhachem <im@hachem.wtf>2026-08-27 14:17:34 +0200
commit705e3914e4e820c3ac92892c32c93479691a09e7 (patch)
treebad8932559620328da060ea45245fcabec2332f2
parent90398614bcc9e1c1bde7a0f65a0db66173a1d8b4 (diff)
add: debugger interface
-rw-r--r--src/core/application.cpp13
-rw-r--r--src/core/application.h3
-rw-r--r--src/core/result.h18
-rw-r--r--src/debugger/debugger.cpp266
-rw-r--r--src/debugger/debugger.h99
-rw-r--r--src/debugger/types.h120
-rw-r--r--src/main.cpp15
7 files changed, 526 insertions, 8 deletions
diff --git a/src/core/application.cpp b/src/core/application.cpp
index 7c2d9a3..1e6d343 100644
--- a/src/core/application.cpp
+++ b/src/core/application.cpp
@@ -4,6 +4,7 @@
#include "core/log.h"
#include "core/window.h"
#include "core/window_manager.h"
+#include "debugger/debugger.h"
#include <glad/gl.h>
@@ -16,9 +17,6 @@ namespace Hsdbg
using Clock = std::chrono::steady_clock;
constexpr float MAX_DELTA_TIME = 0.25f;
-
- // a minimized window has no framebuffer to draw into, so idle instead of
- // burning a core on frames nobody can see
constexpr double MINIMIZED_WAIT_TIMEOUT = 0.1;
}
@@ -41,10 +39,14 @@ namespace Hsdbg
window_spec.vsync = m_spec.vsync;
m_window = &m_window_manager->create_window(window_spec);
+
+ m_debugger = std::make_unique<Debugger>();
}
Application::~Application()
{
+ m_debugger.reset();
+
m_window = nullptr;
m_window_manager.reset();
@@ -103,15 +105,14 @@ namespace Hsdbg
auto Application::update() -> void
{
+ m_debugger->update();
}
auto Application::render() -> void
{
- glViewport(0,
- 0,
+ glViewport(0, 0,
static_cast<GLsizei>(m_window->framebuffer_width()),
static_cast<GLsizei>(m_window->framebuffer_height()));
-
glClearColor(0.09f, 0.09f, 0.11f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
diff --git a/src/core/application.h b/src/core/application.h
index 8df5afb..2f5ffbe 100644
--- a/src/core/application.h
+++ b/src/core/application.h
@@ -6,6 +6,7 @@
namespace Hsdbg
{
+ class Debugger;
class Window;
class WindowManager;
@@ -34,6 +35,7 @@ namespace Hsdbg
auto close() -> void;
auto window() -> Window& { return *m_window; }
+ auto debugger() -> Debugger& { return *m_debugger; }
auto spec() const -> const ApplicationSpec& { return m_spec; }
auto is_running() const -> bool { return m_running; }
auto delta_time() const -> float { return m_delta_time; }
@@ -48,6 +50,7 @@ namespace Hsdbg
ApplicationSpec m_spec;
std::unique_ptr<WindowManager> m_window_manager;
+ std::unique_ptr<Debugger> m_debugger;
Window* m_window = nullptr;
bool m_running = false;
float m_delta_time = 0.0f;
diff --git a/src/core/result.h b/src/core/result.h
new file mode 100644
index 0000000..a516336
--- /dev/null
+++ b/src/core/result.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include <expected>
+#include <format>
+#include <string>
+#include <utility>
+
+namespace Hsdbg
+{
+ template <typename T>
+ using Result = std::expected<T, std::string>;
+
+ template <typename... Args>
+ auto fail(std::format_string<Args...> fmt, Args&&... args) -> std::unexpected<std::string>
+ {
+ return std::unexpected(std::format(fmt, std::forward<Args>(args)...));
+ }
+}
diff --git a/src/debugger/debugger.cpp b/src/debugger/debugger.cpp
new file mode 100644
index 0000000..7cd2d01
--- /dev/null
+++ b/src/debugger/debugger.cpp
@@ -0,0 +1,266 @@
+#include "debugger/debugger.h"
+
+#include "core/log.h"
+
+#include <algorithm>
+#include <source_location>
+
+namespace Hsdbg
+{
+ namespace
+ {
+ // every call site reports itself, so the stubs stay one line each until
+ // there is an lldb session behind them
+ auto not_implemented(std::source_location location = std::source_location::current())
+ -> std::unexpected<std::string>
+ {
+ Log::warn("{} is not implemented yet", location.function_name());
+ return fail("{} is not implemented yet", location.function_name());
+ }
+ }
+
+ Debugger::Debugger()
+ {
+ Log::info("debugger: ready");
+ }
+
+ Debugger::~Debugger()
+ {
+ unload_target();
+ }
+
+ auto Debugger::load_target(const std::filesystem::path& executable) -> Result<void>
+ {
+ std::error_code error;
+
+ if (!std::filesystem::exists(executable, error))
+ return fail("'{}' does not exist", executable.string());
+
+ if (!std::filesystem::is_regular_file(executable, error))
+ return fail("'{}' is not a file", executable.string());
+
+ m_target_path = std::filesystem::absolute(executable, error);
+ if (error)
+ m_target_path = executable;
+
+ m_process_id = 0;
+ m_stop_reason = StopReason::None;
+ set_state(TargetState::Loaded);
+
+ Log::info("debugger: loaded target '{}'", m_target_path.string());
+
+ return {};
+ }
+
+ auto Debugger::unload_target() -> void
+ {
+ m_target_path.clear();
+ m_process_id = 0;
+ m_stop_reason = StopReason::None;
+
+ m_threads.clear();
+ m_call_stack.clear();
+ m_locals.clear();
+ m_registers.clear();
+ m_console_output.clear();
+
+ m_selected_thread = 0;
+ m_selected_frame = 0;
+
+ for (Breakpoint& breakpoint : m_breakpoints)
+ {
+ breakpoint.resolved = false;
+ breakpoint.hit_count = 0;
+ breakpoint.address = 0;
+ }
+
+ set_state(TargetState::NoTarget);
+ }
+
+ auto Debugger::launch(const LaunchSpec& spec) -> Result<void>
+ {
+ (void)spec;
+ return not_implemented();
+ }
+
+ auto Debugger::attach(uint64_t process_id) -> Result<void>
+ {
+ (void)process_id;
+ return not_implemented();
+ }
+
+ auto Debugger::detach() -> Result<void>
+ {
+ return not_implemented();
+ }
+
+ auto Debugger::terminate() -> Result<void>
+ {
+ return not_implemented();
+ }
+
+ auto Debugger::resume() -> Result<void>
+ {
+ return not_implemented();
+ }
+
+ auto Debugger::pause() -> Result<void>
+ {
+ return not_implemented();
+ }
+
+ auto Debugger::step_over(StepMode mode) -> Result<void>
+ {
+ (void)mode;
+ return not_implemented();
+ }
+
+ auto Debugger::step_into(StepMode mode) -> Result<void>
+ {
+ (void)mode;
+ return not_implemented();
+ }
+
+ auto Debugger::step_out() -> Result<void>
+ {
+ return not_implemented();
+ }
+
+ auto Debugger::run_to(const std::filesystem::path& file, uint32_t line) -> Result<void>
+ {
+ (void)file;
+ (void)line;
+ return not_implemented();
+ }
+
+ auto Debugger::add_breakpoint(const std::filesystem::path& file, uint32_t line) -> uint32_t
+ {
+ const auto existing = std::ranges::find_if(m_breakpoints, [&](const Breakpoint& candidate)
+ {
+ return candidate.file == file && candidate.line == line;
+ });
+
+ if (existing != m_breakpoints.end())
+ return existing->id;
+
+ Breakpoint breakpoint;
+ breakpoint.id = m_next_breakpoint_id++;
+ breakpoint.file = file;
+ breakpoint.line = line;
+
+ m_breakpoints.push_back(std::move(breakpoint));
+
+ Log::info("debugger: breakpoint {} at {}:{}",
+ m_breakpoints.back().id,
+ file.filename().string(),
+ line);
+
+ return m_breakpoints.back().id;
+ }
+
+ auto Debugger::add_function_breakpoint(std::string_view function) -> uint32_t
+ {
+ const auto existing = std::ranges::find_if(m_breakpoints, [&](const Breakpoint& candidate)
+ {
+ return candidate.function == function && candidate.line == 0;
+ });
+
+ if (existing != m_breakpoints.end())
+ return existing->id;
+
+ Breakpoint breakpoint;
+ breakpoint.id = m_next_breakpoint_id++;
+ breakpoint.function = function;
+
+ m_breakpoints.push_back(std::move(breakpoint));
+
+ Log::info("debugger: breakpoint {} at {}()", m_breakpoints.back().id, function);
+
+ return m_breakpoints.back().id;
+ }
+
+ auto Debugger::remove_breakpoint(uint32_t id) -> bool
+ {
+ const auto entry = std::ranges::find(m_breakpoints, id, &Breakpoint::id);
+ if (entry == m_breakpoints.end())
+ return false;
+
+ m_breakpoints.erase(entry);
+
+ Log::info("debugger: removed breakpoint {}", id);
+
+ return true;
+ }
+
+ auto Debugger::set_breakpoint_enabled(uint32_t id, bool enabled) -> bool
+ {
+ Breakpoint* breakpoint = find_breakpoint(id);
+ if (breakpoint == nullptr)
+ return false;
+
+ breakpoint->enabled = enabled;
+
+ return true;
+ }
+
+ auto Debugger::clear_breakpoints() -> void
+ {
+ m_breakpoints.clear();
+ }
+
+ auto Debugger::find_breakpoint(uint32_t id) -> Breakpoint*
+ {
+ const auto entry = std::ranges::find(m_breakpoints, id, &Breakpoint::id);
+
+ return entry == m_breakpoints.end() ? nullptr : &*entry;
+ }
+
+ auto Debugger::evaluate(std::string_view expression) -> Result<std::string>
+ {
+ (void)expression;
+ return not_implemented();
+ }
+
+ auto Debugger::read_memory(uint64_t address, size_t size) -> Result<std::vector<uint8_t>>
+ {
+ (void)address;
+ (void)size;
+ return not_implemented();
+ }
+
+ auto Debugger::select_thread(uint64_t thread_id) -> bool
+ {
+ const auto entry = std::ranges::find(m_threads, thread_id, &Thread::id);
+ if (entry == m_threads.end())
+ return false;
+
+ m_selected_thread = thread_id;
+ m_selected_frame = 0;
+
+ return true;
+ }
+
+ auto Debugger::select_frame(uint32_t frame_index) -> bool
+ {
+ if (frame_index >= m_call_stack.size())
+ return false;
+
+ m_selected_frame = frame_index;
+
+ return true;
+ }
+
+ auto Debugger::update() -> void
+ {
+ }
+
+ auto Debugger::set_state(TargetState state) -> void
+ {
+ if (m_state == state)
+ return;
+
+ Log::debug("debugger: {} -> {}", to_string(m_state), to_string(state));
+
+ m_state = state;
+ }
+}
diff --git a/src/debugger/debugger.h b/src/debugger/debugger.h
new file mode 100644
index 0000000..c810417
--- /dev/null
+++ b/src/debugger/debugger.h
@@ -0,0 +1,99 @@
+#pragma once
+
+#include "core/result.h"
+#include "debugger/types.h"
+
+#include <cstdint>
+#include <filesystem>
+#include <span>
+#include <string>
+#include <string_view>
+#include <vector>
+
+namespace Hsdbg
+{
+ class Debugger
+ {
+ public:
+ Debugger();
+ ~Debugger();
+
+ Debugger(const Debugger&) = delete;
+ Debugger(Debugger&&) = delete;
+ auto operator=(const Debugger&) -> Debugger& = delete;
+ auto operator=(Debugger&&) -> Debugger& = delete;
+
+ // target lifetime
+ auto load_target(const std::filesystem::path& executable) -> Result<void>;
+ auto unload_target() -> void;
+ auto launch(const LaunchSpec& spec) -> Result<void>;
+ auto attach(uint64_t process_id) -> Result<void>;
+ auto detach() -> Result<void>;
+ auto terminate() -> Result<void>;
+
+ // execution control
+ auto resume() -> Result<void>;
+ auto pause() -> Result<void>;
+ auto step_over(StepMode mode = StepMode::Line) -> Result<void>;
+ auto step_into(StepMode mode = StepMode::Line) -> Result<void>;
+ auto step_out() -> Result<void>;
+ auto run_to(const std::filesystem::path& file, uint32_t line) -> Result<void>;
+
+ // 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 remove_breakpoint(uint32_t id) -> bool;
+ auto set_breakpoint_enabled(uint32_t id, bool enabled) -> bool;
+ auto clear_breakpoints() -> void;
+ auto find_breakpoint(uint32_t id) -> Breakpoint*;
+ auto breakpoints() const -> std::span<const Breakpoint> { return m_breakpoints; }
+
+ // inspection
+ auto threads() const -> std::span<const Thread> { return m_threads; }
+ 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 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; }
+
+ // 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 selected_thread() const -> uint64_t { return m_selected_thread; }
+ auto selected_frame() const -> uint32_t { return m_selected_frame; }
+
+ // pumps whatever the debug session has to say, called once per frame
+ auto update() -> void;
+
+ auto state() const -> TargetState { return m_state; }
+ auto stop_reason() const -> StopReason { return m_stop_reason; }
+ auto target_path() const -> const std::filesystem::path& { return m_target_path; }
+ auto process_id() const -> uint64_t { return m_process_id; }
+
+ auto has_target() const -> bool { return m_state != TargetState::NoTarget; }
+ auto is_running() const -> bool { return m_state == TargetState::Running; }
+ auto is_stopped() const -> bool { return m_state == TargetState::Stopped; }
+
+ private:
+ auto set_state(TargetState state) -> void;
+
+ TargetState m_state = TargetState::NoTarget;
+ StopReason m_stop_reason = StopReason::None;
+
+ std::filesystem::path m_target_path;
+ uint64_t m_process_id = 0;
+
+ std::vector<Breakpoint> m_breakpoints;
+ uint32_t m_next_breakpoint_id = 1;
+
+ std::vector<Thread> m_threads;
+ std::vector<StackFrame> m_call_stack;
+ std::vector<Variable> m_locals;
+ std::vector<Register> m_registers;
+ std::vector<std::string> m_console_output;
+
+ uint64_t m_selected_thread = 0;
+ uint32_t m_selected_frame = 0;
+ };
+}
diff --git a/src/debugger/types.h b/src/debugger/types.h
new file mode 100644
index 0000000..2f9a698
--- /dev/null
+++ b/src/debugger/types.h
@@ -0,0 +1,120 @@
+#pragma once
+
+#include <cstdint>
+#include <filesystem>
+#include <string>
+#include <string_view>
+#include <vector>
+
+namespace Hsdbg
+{
+ enum class TargetState : uint8_t
+ {
+ NoTarget,
+ Loaded,
+ Launching,
+ Running,
+ Stopped,
+ Exited,
+ Crashed,
+ };
+
+ enum class StopReason : uint8_t
+ {
+ None,
+ Breakpoint,
+ Step,
+ Signal,
+ Exception,
+ Watchpoint,
+ };
+
+ enum class StepMode : uint8_t
+ {
+ Line,
+ Instruction,
+ };
+
+ constexpr auto to_string(TargetState state) -> std::string_view
+ {
+ switch (state)
+ {
+ case TargetState::NoTarget: return "no target";
+ case TargetState::Loaded: return "loaded";
+ case TargetState::Launching: return "launching";
+ case TargetState::Running: return "running";
+ case TargetState::Stopped: return "stopped";
+ case TargetState::Exited: return "exited";
+ case TargetState::Crashed: return "crashed";
+ }
+
+ return "unknown";
+ }
+
+ constexpr auto to_string(StopReason reason) -> std::string_view
+ {
+ switch (reason)
+ {
+ case StopReason::None: return "none";
+ case StopReason::Breakpoint: return "breakpoint";
+ case StopReason::Step: return "step";
+ case StopReason::Signal: return "signal";
+ case StopReason::Exception: return "exception";
+ case StopReason::Watchpoint: return "watchpoint";
+ }
+
+ return "unknown";
+ }
+
+ struct LaunchSpec
+ {
+ std::filesystem::path executable;
+ std::filesystem::path working_directory;
+ std::vector<std::string> arguments;
+ std::vector<std::string> environment;
+ bool stop_at_entry = true;
+ };
+
+ struct Breakpoint
+ {
+ uint32_t id = 0;
+ std::filesystem::path file;
+ uint32_t line = 0;
+ std::string function;
+ uint64_t address = 0;
+ std::string condition;
+ uint32_t hit_count = 0;
+ bool enabled = true;
+ bool resolved = false;
+ };
+
+ struct StackFrame
+ {
+ uint32_t index = 0;
+ uint64_t program_counter = 0;
+ std::string function;
+ std::filesystem::path file;
+ uint32_t line = 0;
+ };
+
+ struct Thread
+ {
+ uint64_t id = 0;
+ std::string name;
+ StopReason stop_reason = StopReason::None;
+ };
+
+ struct Variable
+ {
+ std::string name;
+ std::string type;
+ std::string value;
+ std::vector<Variable> children;
+ };
+
+ struct Register
+ {
+ std::string name;
+ uint64_t value = 0;
+ };
+}
diff --git a/src/main.cpp b/src/main.cpp
index bb72b0b..b96a74a 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,9 +1,13 @@
#include "core/application.h"
+#include "core/log.h"
+
+#include "debugger/debugger.h"
+
+#include <span>
auto main(int argc, char** argv) -> int
{
- (void)argc;
- (void)argv;
+ const std::span arguments(argv, static_cast<size_t>(argc));
Hsdbg::ApplicationSpec spec;
spec.name = "hsdbg";
@@ -11,6 +15,13 @@ auto main(int argc, char** argv) -> int
spec.height = 900;
Hsdbg::Application app(spec);
+
+ if (arguments.size() > 1)
+ {
+ if (const auto result = app.debugger().load_target(arguments[1]); !result)
+ Hsdbg::Log::error("{}", result.error());
+ }
+
app.run();
return 0;