diff options
| -rw-r--r-- | .gitignore | 5 | ||||
| -rw-r--r-- | premake5.lua | 195 | ||||
| -rw-r--r-- | src/debugger/debugger.cpp | 250 | ||||
| -rw-r--r-- | src/debugger/debugger.h | 8 | ||||
| -rw-r--r-- | src/debugger/types.h | 1 | ||||
| -rw-r--r-- | src/ui/source_view.cpp | 12 | ||||
| -rw-r--r-- | src/ui/ui.cpp | 31 | ||||
| -rw-r--r-- | src/ui/ui.h | 1 |
8 files changed, 492 insertions, 11 deletions
@@ -18,3 +18,8 @@ compile_flags.txt # imgui layout state imgui.ini + +# llvm pulled down by `premake5 fetch-llvm` +ext/llvm/ +ext/llvm-staging/ +ext/*.tar.xz diff --git a/premake5.lua b/premake5.lua index eff7f87..ffda3a8 100644 --- a/premake5.lua +++ b/premake5.lua @@ -1,12 +1,114 @@ output_dir = "%{cfg.buildcfg}-%{cfg.system}" +llvm_version = "22.1.0" +llvm_vendored_dir = "ext/llvm" + +function llvm_release_asset() + local host = os.host() + local arm = os.hostarch() == "ARM64" or os.hostarch() == "arm64" + + if host == "macosx" then + return "LLVM-" .. llvm_version .. "-macOS-ARM64.tar.xz" + elseif host == "linux" then + return "LLVM-" .. llvm_version .. (arm and "-Linux-ARM64" or "-Linux-X64") .. ".tar.xz" + elseif host == "windows" then + return "clang+llvm-" .. llvm_version .. + (arm and "-aarch64-pc-windows-msvc" or "-x86_64-pc-windows-msvc") .. ".tar.xz" + end + + return nil +end + +function llvm_prefix() + local function usable(candidate) + if candidate == nil or candidate == "" then + return nil + end + + local prefix = path.getabsolute( + (candidate:gsub("%s+$", "")):gsub("\\", "/"), _MAIN_SCRIPT_DIR) + + return os.isdir(prefix .. "/include/lldb") and prefix or nil + end + + local function newest_of(pattern) + local matches = os.matchdirs(pattern) + table.sort(matches) + + for index = #matches, 1, -1 do + local prefix = usable(matches[index]) + + if prefix ~= nil then + return prefix + end + end + + return nil + end + + local prefix = usable(os.getenv("LLVM_PREFIX")) + or usable(llvm_vendored_dir) + or usable(os.outputof("llvm-config --prefix")) + + if prefix == nil and os.host() == "windows" then + prefix = usable(os.getenv("ProgramFiles") and os.getenv("ProgramFiles") .. "/LLVM") + or newest_of("C:/Program Files/LLVM*") + end + + if prefix == nil and os.host() == "macosx" then + prefix = usable(os.outputof("brew --prefix llvm")) + end + + if prefix == nil and os.host() == "linux" then + prefix = newest_of("/usr/lib/llvm-*") or newest_of("/usr/local/llvm*") + end + + return prefix +end + +llvm_dir = llvm_prefix() + +if llvm_dir == nil and _ACTION ~= nil and _ACTION ~= "fetch-llvm" and _ACTION ~= "clean" then + error("no llvm with lldb headers found. install one and set LLVM_PREFIX, " .. + "or run `premake5 fetch-llvm` to download the pinned " .. llvm_version .. " release") +end + +llvm_dir = llvm_dir or "" + include_dir = { glfw = "ext/glfw/include", glad = "ext/glad/include", imgui = "ext/imgui", imgui_backends = "ext/imgui/backends", + lldb = llvm_dir .. "/include", } +library_dir = { + lldb = llvm_dir .. "/lib", +} + +function lldb_library() + if llvm_dir == "" then + return "" + end + + local names = { "liblldb.dylib", "liblldb.so", "liblldb.lib" } + + for _, name in ipairs(names) do + local candidate = library_dir.lldb .. "/" .. name + + if os.isfile(candidate) then + return candidate + end + end + + local versioned = os.matchfiles(library_dir.lldb .. "/liblldb.so.*") + table.sort(versioned) + + return versioned[#versioned] + or error("llvm at " .. llvm_dir .. " has lldb headers but no liblldb in " .. library_dir.lldb) +end + function setup_target() targetdir ("bin/" .. output_dir) objdir ("bin-int/" .. output_dir .. "/%{prj.name}") @@ -40,16 +142,93 @@ newaction { execute = function() os.rmdir("bin") os.rmdir("bin-int") + os.rmdir(".vs") os.remove("Makefile") - for _, file in ipairs(os.matchfiles("*.make")) do - os.remove(file) + local patterns = { "*.make", "*.sln", "*.vcxproj", "*.vcxproj.*" } + + for _, pattern in ipairs(patterns) do + for _, file in ipairs(os.matchfiles(pattern)) do + os.remove(file) + end end print("Cleaned build output and generated project files") end } +newaction { + trigger = "fetch-llvm", + description = "Download the pinned llvm release into " .. llvm_vendored_dir, + + execute = function() + if os.isdir(llvm_vendored_dir .. "/include/lldb") then + print(llvm_vendored_dir .. " already has an llvm with lldb headers") + return + end + + local asset = llvm_release_asset() + + if asset == nil then + error("no pinned llvm release for " .. os.host() .. " " .. os.hostarch() .. + ", install llvm yourself and set LLVM_PREFIX") + end + + local archive = "ext/" .. asset + local staging = "ext/llvm-staging" + + if not os.isfile(archive) then + local url = "https://github.com/llvm/llvm-project/releases/download/llvmorg-" .. + llvm_version .. "/" .. asset + + print("downloading " .. url) + + local reported = 0 + local result, code = http.download(url, archive, { + progress = function(total, current) + if total > 0 and current - reported >= total / 20 then + reported = current + printf(" %d%% (%.0f mb)", math.floor(current / total * 100), current / 1048576) + end + end + }) + + if result ~= "OK" then + os.remove(archive) + error("download failed with " .. tostring(code) .. ": " .. tostring(result)) + end + end + + os.rmdir(staging) + os.mkdir(staging) + + print("extracting " .. asset) + + local extracted = os.execute('tar -xf "' .. archive .. '" -C "' .. staging .. '"') + + if extracted ~= true and extracted ~= 0 then + error("could not extract " .. archive .. ", unpack it into " .. llvm_vendored_dir .. " by hand") + end + + local unpacked = os.matchdirs(staging .. "/*")[1] + + if unpacked == nil then + error(archive .. " did not contain an llvm directory") + end + + os.rmdir(llvm_vendored_dir) + + if not os.rename(unpacked, llvm_vendored_dir) then + error("could not move " .. unpacked .. " to " .. llvm_vendored_dir) + end + + os.rmdir(staging) + os.remove(archive) + + print("llvm " .. llvm_version .. " is ready in " .. llvm_vendored_dir) + end +} + workspace "hsdbg" architecture "x64" @@ -265,6 +444,7 @@ project "hsdbg" "%{include_dir.glad}", "%{include_dir.imgui}", "%{include_dir.imgui_backends}", + "%{include_dir.lldb}", } links { @@ -273,9 +453,20 @@ project "hsdbg" "imgui", } + defines { "HSDBG_LLVM_PREFIX=\"" .. llvm_dir .. "\"" } + + linkoptions { lldb_library() } + filter "system:windows" systemversion "latest" + links "opengl32" + + postbuildcommands { + '{COPYFILE} "' .. llvm_dir .. '/bin/liblldb.dll" "%{cfg.targetdir}"', + } + filter "system:not windows" + linkoptions { "-Wl,-rpath," .. library_dir.lldb } filter "system:linux" links { "GL", diff --git a/src/debugger/debugger.cpp b/src/debugger/debugger.cpp index 7cd2d01..1e1aee7 100644 --- a/src/debugger/debugger.cpp +++ b/src/debugger/debugger.cpp @@ -1,14 +1,116 @@ #include "debugger/debugger.h" +#include "core/assert.h" #include "core/log.h" +#include <lldb/API/LLDB.h> + #include <algorithm> #include <source_location> +// no standard way to read or write the environment +#include <cstdlib> + namespace Hsdbg { namespace { +#ifndef HSDBG_LLVM_PREFIX + #define HSDBG_LLVM_PREFIX "" +#endif + + auto set_environment(const char* name, const std::string& value) -> void + { +#ifdef HSDBG_WINDOWS + _putenv_s(name, value.c_str()); +#else + setenv(name, value.c_str(), 1); +#endif + } + + // lldb spawns a helper to control the inferior on unix. windows needs none, + // linux ships lldb-server beside liblldb, and macos requires one entitled + // with com.apple.private.cs.debugger, which only the xcode copy carries + auto debug_server_candidates() -> std::vector<std::filesystem::path> + { + const std::filesystem::path llvm_prefix(HSDBG_LLVM_PREFIX); + +#if defined(HSDBG_MACOS) + return { + "/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Versions/A/" + "Resources/debugserver", + "/Library/Developer/CommandLineTools/Library/PrivateFrameworks/LLDB.framework/" + "Resources/debugserver", + llvm_prefix / "bin" / "debugserver", + }; +#elif defined(HSDBG_LINUX) + return { llvm_prefix / "bin" / "lldb-server" }; +#else + return {}; +#endif + } + + auto adopt_debug_server() -> void + { + const std::vector<std::filesystem::path> candidates = debug_server_candidates(); + + if (candidates.empty()) + return; + + if (std::getenv("LLDB_DEBUGSERVER_PATH") != nullptr) + return; + + for (const std::filesystem::path& candidate : candidates) + { + std::error_code error; + + if (!std::filesystem::exists(candidate, error)) + continue; + + set_environment("LLDB_DEBUGSERVER_PATH", candidate.string()); + Log::debug("debugger: using debug server '{}'", candidate.string()); + + return; + } + + Log::warn("debugger: no debug server found, launching a target will fail"); + } + + auto read_back(lldb::SBTarget& target, lldb::SBBreakpoint& source, Breakpoint& breakpoint) -> void + { + breakpoint.enabled = source.IsEnabled(); + breakpoint.hit_count = source.GetHitCount(); + + // lldb only calls a location resolved once a process has it mapped, but + // for the ui the question is whether it found somewhere to put it at all + breakpoint.resolved = source.GetNumLocations() > 0; + + if (source.GetNumLocations() == 0) + { + breakpoint.address = 0; + return; + } + + lldb::SBBreakpointLocation location = source.GetLocationAtIndex(0); + lldb::SBAddress address = location.GetAddress(); + + const lldb::addr_t load_address = address.GetLoadAddress(target); + breakpoint.address = load_address != LLDB_INVALID_ADDRESS ? load_address + : address.GetFileAddress(); + + lldb::SBSymbolContext context = address.GetSymbolContext(lldb::eSymbolContextEverything); + + if (const char* name = context.GetFunction().GetName(); name != nullptr) + breakpoint.function = name; + else if (const char* symbol = context.GetSymbol().GetName(); symbol != nullptr) + breakpoint.function = symbol; + + lldb::SBLineEntry line_entry = context.GetLineEntry(); + + if (breakpoint.line != 0 && line_entry.IsValid()) + breakpoint.line = line_entry.GetLine(); + } + // 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()) @@ -19,14 +121,37 @@ namespace Hsdbg } } + struct Debugger::Session + { + lldb::SBDebugger debugger; + lldb::SBTarget target; + }; + Debugger::Debugger() + : m_session(std::make_unique<Session>()) { - Log::info("debugger: ready"); + adopt_debug_server(); + + lldb::SBDebugger::Initialize(); + + m_session->debugger = lldb::SBDebugger::Create(); + m_session->debugger.SetAsync(true); + + HSDBG_ASSERT(m_session->debugger.IsValid(), "failed to create the lldb debugger"); + + Log::info("debugger: {}", lldb::SBDebugger::GetVersionString()); } Debugger::~Debugger() { unload_target(); + + if (m_session->debugger.IsValid()) + lldb::SBDebugger::Destroy(m_session->debugger); + + m_session.reset(); + + lldb::SBDebugger::Terminate(); } auto Debugger::load_target(const std::filesystem::path& executable) -> Result<void> @@ -43,11 +168,42 @@ namespace Hsdbg if (error) m_target_path = executable; + lldb::SBError create_error; + lldb::SBTarget target = m_session->debugger.CreateTarget(m_target_path.string().c_str(), + nullptr, + nullptr, + true, + create_error); + + if (!target.IsValid()) + { + const char* reason = create_error.GetCString(); + + m_target_path.clear(); + + return fail("could not load '{}': {}", + executable.string(), + reason != nullptr ? reason : "unknown error"); + } + + if (m_session->target.IsValid()) + m_session->debugger.DeleteTarget(m_session->target); + + m_session->target = target; + m_process_id = 0; m_stop_reason = StopReason::None; set_state(TargetState::Loaded); - Log::info("debugger: loaded target '{}'", m_target_path.string()); + const char* triple = m_session->target.GetTriple(); + + Log::info("debugger: loaded target '{}' ({})", + m_target_path.string(), + triple != nullptr ? triple : "unknown triple"); + + // breakpoints set before a target existed are only requests until now + for (Breakpoint& breakpoint : m_breakpoints) + resolve_breakpoint(breakpoint); return {}; } @@ -69,11 +225,19 @@ namespace Hsdbg for (Breakpoint& breakpoint : m_breakpoints) { + breakpoint.backend_id = 0; breakpoint.resolved = false; breakpoint.hit_count = 0; breakpoint.address = 0; } + if (m_session->target.IsValid()) + { + m_session->target.DeleteAllBreakpoints(); + m_session->debugger.DeleteTarget(m_session->target); + m_session->target = lldb::SBTarget(); + } + set_state(TargetState::NoTarget); } @@ -149,6 +313,7 @@ namespace Hsdbg breakpoint.line = line; m_breakpoints.push_back(std::move(breakpoint)); + resolve_breakpoint(m_breakpoints.back()); Log::info("debugger: breakpoint {} at {}:{}", m_breakpoints.back().id, @@ -173,6 +338,7 @@ namespace Hsdbg breakpoint.function = function; m_breakpoints.push_back(std::move(breakpoint)); + resolve_breakpoint(m_breakpoints.back()); Log::info("debugger: breakpoint {} at {}()", m_breakpoints.back().id, function); @@ -185,6 +351,9 @@ namespace Hsdbg if (entry == m_breakpoints.end()) return false; + if (entry->backend_id != 0 && m_session->target.IsValid()) + m_session->target.BreakpointDelete(entry->backend_id); + m_breakpoints.erase(entry); Log::info("debugger: removed breakpoint {}", id); @@ -200,11 +369,22 @@ namespace Hsdbg breakpoint->enabled = enabled; + if (breakpoint->backend_id != 0 && m_session->target.IsValid()) + { + lldb::SBBreakpoint source = m_session->target.FindBreakpointByID(breakpoint->backend_id); + + if (source.IsValid()) + source.SetEnabled(enabled); + } + return true; } auto Debugger::clear_breakpoints() -> void { + if (m_session->target.IsValid()) + m_session->target.DeleteAllBreakpoints(); + m_breakpoints.clear(); } @@ -252,6 +432,72 @@ namespace Hsdbg auto Debugger::update() -> void { + sync_breakpoints(); + } + + auto Debugger::resolve_breakpoint(Breakpoint& breakpoint) -> void + { + if (!m_session->target.IsValid()) + return; + + lldb::SBBreakpoint created; + + if (breakpoint.line == 0) + { + // unscoped, a name like main matches every module the target pulls in + created = m_session->target.BreakpointCreateByName( + breakpoint.function.c_str(), m_session->target.GetExecutable().GetFilename()); + } + else + { + created = m_session->target.BreakpointCreateByLocation(breakpoint.file.string().c_str(), + breakpoint.line); + + // debug info records whatever path the compiler saw, which rarely + // matches what the user opened, so fall back to the file name alone + if (created.GetNumLocations() == 0 && breakpoint.file.has_parent_path()) + { + m_session->target.BreakpointDelete(created.GetID()); + + created = m_session->target.BreakpointCreateByLocation( + breakpoint.file.filename().string().c_str(), breakpoint.line); + } + } + + if (!created.IsValid()) + { + Log::warn("debugger: lldb refused breakpoint {}", breakpoint.id); + return; + } + + created.SetEnabled(breakpoint.enabled); + + breakpoint.backend_id = created.GetID(); + + read_back(m_session->target, created, breakpoint); + + Log::debug("debugger: breakpoint {} {} ({} locations)", + breakpoint.id, + breakpoint.resolved ? "resolved" : "pending", + created.GetNumLocations()); + } + + auto Debugger::sync_breakpoints() -> void + { + if (!m_session->target.IsValid()) + return; + + for (Breakpoint& breakpoint : m_breakpoints) + { + if (breakpoint.backend_id == 0) + continue; + + lldb::SBBreakpoint source = m_session->target.FindBreakpointByID(breakpoint.backend_id); + if (!source.IsValid()) + continue; + + read_back(m_session->target, source, breakpoint); + } } auto Debugger::set_state(TargetState state) -> void diff --git a/src/debugger/debugger.h b/src/debugger/debugger.h index c810417..c4f3431 100644 --- a/src/debugger/debugger.h +++ b/src/debugger/debugger.h @@ -5,6 +5,7 @@ #include <cstdint> #include <filesystem> +#include <memory> #include <span> #include <string> #include <string_view> @@ -76,7 +77,14 @@ namespace Hsdbg auto is_stopped() const -> bool { return m_state == TargetState::Stopped; } private: + // keeps the lldb headers out of everything that talks to the debugger + struct Session; + auto set_state(TargetState state) -> void; + auto resolve_breakpoint(Breakpoint& breakpoint) -> void; + auto sync_breakpoints() -> void; + + std::unique_ptr<Session> m_session; TargetState m_state = TargetState::NoTarget; StopReason m_stop_reason = StopReason::None; diff --git a/src/debugger/types.h b/src/debugger/types.h index 2f9a698..193a1cc 100644 --- a/src/debugger/types.h +++ b/src/debugger/types.h @@ -78,6 +78,7 @@ namespace Hsdbg struct Breakpoint { uint32_t id = 0; + int32_t backend_id = 0; std::filesystem::path file; uint32_t line = 0; std::string function; diff --git a/src/ui/source_view.cpp b/src/ui/source_view.cpp index 37ceffc..7f80aa8 100644 --- a/src/ui/source_view.cpp +++ b/src/ui/source_view.cpp @@ -192,10 +192,14 @@ namespace Hsdbg if (breakpoint != nullptr) { - draw_list->AddCircleFilled(marker_center, - BREAKPOINT_RADIUS, - breakpoint->enabled ? BREAKPOINT_COLOR - : BREAKPOINT_DISABLED_COLOR); + const ImU32 color = breakpoint->enabled ? BREAKPOINT_COLOR + : BREAKPOINT_DISABLED_COLOR; + + // hollow until lldb finds somewhere to actually put it + 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) { diff --git a/src/ui/ui.cpp b/src/ui/ui.cpp index 91fab0a..bc7c910 100644 --- a/src/ui/ui.cpp +++ b/src/ui/ui.cpp @@ -28,8 +28,8 @@ namespace Hsdbg constexpr const char* LOAD_TARGET_POPUP = "load target"; - // macos gives us a 4.1 core context, which speaks glsl 1.50 - // #portability + // the lowest glsl that every 3.2+ core profile accepts, and the window + // asks for a 3.3 core context on all three platforms constexpr const char* GLSL_VERSION = "#version 150"; constexpr size_t MAX_CONSOLE_LINES = 2048; @@ -173,6 +173,9 @@ namespace Hsdbg { build_default_layout(dockspace_id); m_layout_built = true; + + // otherwise whichever panel happens to be submitted last wins the tab + m_focus_breakpoints = true; } draw_status_bar(debugger); @@ -190,6 +193,14 @@ namespace Hsdbg if (m_visible.demo) ImGui::ShowDemoWindow(&m_visible.demo); + + // 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) + { + ImGui::SetWindowFocus(PANEL_BREAKPOINTS); + m_focus_breakpoints = false; + } } auto Ui::build_default_layout(uint32_t dockspace_id) -> void @@ -466,11 +477,12 @@ namespace Hsdbg ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_ScrollY; - if (ImGui::BeginTable("##breakpoints", 5, flags)) + if (ImGui::BeginTable("##breakpoints", 6, flags)) { ImGui::TableSetupColumn("on", ImGuiTableColumnFlags_WidthFixed, 26.0f); ImGui::TableSetupColumn("id", ImGuiTableColumnFlags_WidthFixed, 30.0f); ImGui::TableSetupColumn("location"); + ImGui::TableSetupColumn("address"); ImGui::TableSetupColumn("hits", ImGuiTableColumnFlags_WidthFixed, 40.0f); ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 26.0f); ImGui::TableSetupScrollFreeze(0, 1); @@ -507,6 +519,19 @@ namespace Hsdbg ImGui::SetTooltip("%s", breakpoint.file.string().c_str()); ImGui::TableNextColumn(); + if (breakpoint.resolved) + { + ImGui::Text("0x%llx", static_cast<unsigned long long>(breakpoint.address)); + + if (ImGui::IsItemHovered() && !breakpoint.function.empty()) + ImGui::SetTooltip("%s", breakpoint.function.c_str()); + } + else + { + ImGui::TextDisabled("pending"); + } + + ImGui::TableNextColumn(); ImGui::Text("%u", breakpoint.hit_count); ImGui::TableNextColumn(); diff --git a/src/ui/ui.h b/src/ui/ui.h index 20d4e1b..90c314d 100644 --- a/src/ui/ui.h +++ b/src/ui/ui.h @@ -75,5 +75,6 @@ namespace Hsdbg bool m_layout_built = false; bool m_console_scroll_pending = false; bool m_load_target_pending = false; + bool m_focus_breakpoints = false; }; } |
