aboutsummaryrefslogtreecommitdiff
path: root/src/debugger
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 /src/debugger
parent90398614bcc9e1c1bde7a0f65a0db66173a1d8b4 (diff)
add: debugger interface
Diffstat (limited to 'src/debugger')
-rw-r--r--src/debugger/debugger.cpp266
-rw-r--r--src/debugger/debugger.h99
-rw-r--r--src/debugger/types.h120
3 files changed, 485 insertions, 0 deletions
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;
+ };
+}