diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/core/application.cpp | 13 | ||||
| -rw-r--r-- | src/core/application.h | 3 | ||||
| -rw-r--r-- | src/core/result.h | 18 | ||||
| -rw-r--r-- | src/debugger/debugger.cpp | 266 | ||||
| -rw-r--r-- | src/debugger/debugger.h | 99 | ||||
| -rw-r--r-- | src/debugger/types.h | 120 | ||||
| -rw-r--r-- | src/main.cpp | 15 |
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; |
