diff options
| author | hachem <im@hachem.wtf> | 2026-09-04 03:21:45 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-09-04 03:21:45 +0200 |
| commit | 9023d106c61db0be0de6460a397bbad42bbc3d96 (patch) | |
| tree | c1773245e02b507058d65d031c258c2a24601047 /src/debugger | |
| parent | 0c3cb5bc5fa54e2d6c49fad746871b0ed0da66af (diff) | |
feat: macro expantion
Diffstat (limited to 'src/debugger')
| -rw-r--r-- | src/debugger/debugger.cpp | 519 | ||||
| -rw-r--r-- | src/debugger/debugger.h | 97 | ||||
| -rw-r--r-- | src/debugger/types.h | 21 |
3 files changed, 609 insertions, 28 deletions
diff --git a/src/debugger/debugger.cpp b/src/debugger/debugger.cpp index 392dc19..d5dccee 100644 --- a/src/debugger/debugger.cpp +++ b/src/debugger/debugger.cpp @@ -742,11 +742,33 @@ namespace Hsdbg m_process_id = process.GetProcessID(); m_stop_reason = StopReason::None; - // each run is timed from its own start, so drop whatever the last run left + // each run is timed from its own start, so drop whatever the last run left, + // including return breakpoints whose addresses die with the old process m_trace_epoch = std::chrono::steady_clock::now(); + clear_return_breakpoints(); + m_timeline.clear(); + + // instrumentation lives in the process, so a fresh run re-resolves it and + // starts its record count and per-thread stacks over + m_instr_checked = false; + m_instr_available = false; + m_instr_read_count = 0; + m_instr_base_set = false; + m_instr_functions.clear(); + m_instr_names.clear(); + m_instr_stacks.clear(); + + m_sample_pending = false; + m_sample_functions.clear(); + m_sample_stacks.clear(); + for (FunctionTrace& trace : m_traces) { trace.call_count = 0; + trace.completed_count = 0; + trace.total_time = 0.0; + trace.min_time = 0.0; + trace.max_time = 0.0; trace.calls.clear(); } @@ -1263,6 +1285,7 @@ namespace Hsdbg pump_events(); sync_breakpoints(); sample_process_stats(); + maybe_request_sample(); } auto Debugger::sample_process_stats() -> void @@ -1359,6 +1382,34 @@ namespace Hsdbg m_traces.clear(); } + auto Debugger::ensure_return_breakpoint(uint64_t address) -> int32_t + { + if (const auto entry = m_return_breakpoints.find(address); entry != m_return_breakpoints.end()) + return entry->second; + + lldb::SBBreakpoint created = m_session->target.BreakpointCreateByAddress(address); + if (!created.IsValid()) + return 0; + + created.SetEnabled(true); + + const int32_t id = created.GetID(); + m_return_breakpoints.emplace(address, id); + return id; + } + + auto Debugger::clear_return_breakpoints() -> void + { + if (m_session->target.IsValid()) + { + for (const auto& [address, id] : m_return_breakpoints) + m_session->target.BreakpointDelete(id); + } + + m_return_breakpoints.clear(); + m_pending_calls.clear(); + } + auto Debugger::handle_trace_stop() -> bool { if (m_traces.empty()) @@ -1377,39 +1428,44 @@ namespace Hsdbg const lldb::StopReason reason = thread.GetStopReason(); - if (reason == lldb::eStopReasonBreakpoint) + if (reason != lldb::eStopReasonBreakpoint) { - // the stop reason carries (breakpoint id, location id) pairs - const size_t pairs = thread.GetStopReasonDataCount() / 2; + // a signal, exception or completed step: a genuine stop + if (reason != lldb::eStopReasonNone && reason != lldb::eStopReasonInvalid) + any_other = true; - for (size_t pair = 0; pair < pairs; ++pair) - { - const auto backend_id = - static_cast<int32_t>(thread.GetStopReasonDataAtIndex(pair * 2)); + continue; + } - const auto trace = std::ranges::find(m_traces, backend_id, - &FunctionTrace::entry_backend_id); + // the stop reason carries (breakpoint id, location id) pairs + const size_t pairs = thread.GetStopReasonDataCount() / 2; - if (trace != m_traces.end() && backend_id != 0) - { - trace->call_count += 1; + for (size_t pair = 0; pair < pairs; ++pair) + { + const auto backend_id = + static_cast<int32_t>(thread.GetStopReasonDataAtIndex(pair * 2)); - if (trace->calls.size() < MAX_TRACE_CALLS) - trace->calls.push_back(TraceCall{ trace_now(), 0.0 }); + const auto trace = std::ranges::find(m_traces, backend_id, + &FunctionTrace::entry_backend_id); - any_trace = true; - } - else - { - // a real breakpoint the user set - any_other = true; - } + const bool is_return = std::ranges::any_of(m_return_breakpoints, + [&](const auto& kv) { return kv.second == backend_id; }); + + if (trace != m_traces.end() && backend_id != 0) + { + record_trace_entry(*trace, thread); + any_trace = true; + } + else if (is_return) + { + record_trace_return(thread); + any_trace = true; + } + else + { + // a real breakpoint the user set + any_other = true; } - } - else if (reason != lldb::eStopReasonNone && reason != lldb::eStopReasonInvalid) - { - // a signal, exception or completed step: a genuine stop - any_other = true; } } @@ -1423,6 +1479,402 @@ namespace Hsdbg return false; } + auto Debugger::record_trace_entry(FunctionTrace& trace, lldb::SBThread& thread) -> void + { + const double start = trace_now(); + + trace.call_count += 1; + + size_t call_index = MAX_TRACE_CALLS; + if (trace.calls.size() < MAX_TRACE_CALLS) + { + call_index = trace.calls.size(); + trace.calls.push_back(TraceCall{ start, 0.0 }); + } + + // the breakpoint sits past the prologue, so frame 1 is a settled caller + // whose pc is the address this call will return to + lldb::SBFrame caller = thread.GetFrameAtIndex(1); + if (!caller.IsValid()) + return; + + const uint64_t thread_id = thread.GetThreadID(); + + // nesting is how many traced calls are already open on this thread; the + // outermost one sits on row zero and children stack above it + const auto depth = static_cast<uint32_t>(std::ranges::count( + m_pending_calls, thread_id, &PendingCall::thread_id)); + + size_t span_index = MAX_TRACE_CALLS; + if (m_timeline.size() < MAX_TRACE_CALLS) + { + span_index = m_timeline.size(); + m_timeline.push_back(TimelineSpan{ trace.id, thread_id, start, 0.0, depth }); + } + + PendingCall pending; + pending.trace_id = trace.id; + pending.thread_id = thread_id; + pending.return_pc = caller.GetPC(); + pending.frame_sp = caller.GetSP(); + pending.start = start; + pending.call_index = call_index; + pending.span_index = span_index; + + m_pending_calls.push_back(pending); + ensure_return_breakpoint(pending.return_pc); + } + + auto Debugger::record_trace_return(lldb::SBThread& thread) -> void + { + lldb::SBFrame frame = thread.GetFrameAtIndex(0); + if (!frame.IsValid()) + return; + + const uint64_t pc = frame.GetPC(); + const uint64_t sp = frame.GetSP(); + const uint64_t thread_id = thread.GetThreadID(); + const double now = trace_now(); + + // walk newest first so a recursive call matches its own activation + for (auto entry = m_pending_calls.rbegin(); entry != m_pending_calls.rend(); ++entry) + { + if (entry->thread_id != thread_id || entry->return_pc != pc || entry->frame_sp != sp) + continue; + + const double duration = now - entry->start; + + if (const auto trace = std::ranges::find(m_traces, entry->trace_id, &FunctionTrace::id); + trace != m_traces.end()) + { + if (entry->call_index < trace->calls.size()) + trace->calls[entry->call_index].duration = duration; + + if (entry->span_index < m_timeline.size()) + m_timeline[entry->span_index].duration = duration; + + trace->completed_count += 1; + trace->total_time += duration; + trace->max_time = std::max(trace->max_time, duration); + trace->min_time = trace->completed_count == 1 ? duration + : std::min(trace->min_time, duration); + } + + m_pending_calls.erase(std::next(entry).base()); + break; + } + } + + // mirrors HsdbgTraceRecord in ext/hsdbg_trace/hsdbg_trace.h; that header is the + // source of truth for this 32-byte layout + struct Debugger::InstrRecord + { + uint64_t timestamp_ns; + uint64_t function; + uint64_t thread_id; + uint32_t kind; + uint32_t reserved; + }; + + auto Debugger::symbol_load_address(const char* name) -> uint64_t + { + lldb::SBSymbolContextList list = m_session->target.FindSymbols(name); + + for (uint32_t index = 0; index < list.GetSize(); ++index) + { + lldb::SBSymbol symbol = list.GetContextAtIndex(index).GetSymbol(); + if (!symbol.IsValid()) + continue; + + const uint64_t load = symbol.GetStartAddress().GetLoadAddress(m_session->target); + if (load != LLDB_INVALID_ADDRESS && load != 0) + return load; + } + + return 0; + } + + auto Debugger::resolve_instrumentation() -> void + { + const uint64_t head = symbol_load_address("hsdbg_trace_head"); + const uint64_t records = symbol_load_address("hsdbg_trace_records"); + const uint64_t capacity_addr = symbol_load_address("hsdbg_trace_capacity"); + + if (head == 0 || records == 0 || capacity_addr == 0) + { + // the target simply was not built with the trace runtime + m_instr_checked = true; + m_instr_available = false; + return; + } + + lldb::SBError error; + uint64_t capacity = 0; + m_session->process.ReadMemory(capacity_addr, &capacity, sizeof(capacity), error); + + if (!error.Success() || capacity == 0) + return; // memory not readable yet, try again on the next stop + + m_instr_head_addr = head; + m_instr_records_addr = records; + m_instr_capacity = capacity; + m_instr_available = true; + m_instr_checked = true; + + Log::info("debugger: function instrumentation active ({} record buffer)", capacity); + } + + auto Debugger::read_instrumentation() -> void + { + if (!is_alive(m_session->process)) + return; + + if (!m_instr_checked) + resolve_instrumentation(); + + if (!m_instr_available) + return; + + lldb::SBError error; + uint64_t head = 0; + m_session->process.ReadMemory(m_instr_head_addr, &head, sizeof(head), error); + + if (!error.Success() || head <= m_instr_read_count) + return; + + const uint64_t capacity = m_instr_capacity; + + // if we fell further behind than the ring holds, only the newest survive + uint64_t start = m_instr_read_count; + if (head - start > capacity) + start = head - capacity; + + const uint64_t total = head - start; + std::vector<InstrRecord> buffer(total); + + // the live records wrap around the ring, so read up to two flat chunks + uint64_t read = 0; + while (read < total) + { + const uint64_t position = (start + read) % capacity; + const uint64_t chunk = std::min(total - read, capacity - position); + const uint64_t address = m_instr_records_addr + position * sizeof(InstrRecord); + + lldb::SBError chunk_error; + const size_t got = m_session->process.ReadMemory( + address, buffer.data() + read, chunk * sizeof(InstrRecord), chunk_error); + + if (!chunk_error.Success() || got != chunk * sizeof(InstrRecord)) + return; // leave m_instr_read_count untouched and retry next stop + + read += chunk; + } + + for (const InstrRecord& record : buffer) + apply_instr_record(record); + + m_instr_read_count = head; + } + + auto Debugger::apply_instr_record(const InstrRecord& record) -> void + { + if (!m_instr_base_set) + { + m_instr_base_ns = record.timestamp_ns; + m_instr_base_set = true; + } + + const double start = static_cast<double>(record.timestamp_ns - m_instr_base_ns) / 1.0e9; + const uint32_t id = intern_instr_function(record.function); + + std::vector<InstrOpenCall>& stack = m_instr_stacks[record.thread_id]; + + if (record.kind == 0 /* enter */) + { + const auto depth = static_cast<uint32_t>(stack.size()); + + size_t span_index = m_timeline.size(); + if (m_timeline.size() < MAX_TRACE_CALLS) + m_timeline.push_back(TimelineSpan{ id, record.thread_id, start, 0.0, depth }); + + stack.push_back(InstrOpenCall{ id, record.timestamp_ns, span_index }); + } + else if (!stack.empty()) + { + const InstrOpenCall open = stack.back(); + stack.pop_back(); + + const double duration = + static_cast<double>(record.timestamp_ns - open.start_ns) / 1.0e9; + + if (open.span_index < m_timeline.size()) + m_timeline[open.span_index].duration = duration; + } + } + + auto Debugger::intern_instr_function(uint64_t address) -> uint32_t + { + if (const auto entry = m_instr_functions.find(address); entry != m_instr_functions.end()) + return entry->second; + + std::string name; + lldb::SBAddress resolved = m_session->target.ResolveLoadAddress(address); + + if (resolved.IsValid()) + { + if (lldb::SBFunction function = resolved.GetFunction(); + function.IsValid() && function.GetName() != nullptr) + { + name = function.GetName(); + } + else if (lldb::SBSymbol symbol = resolved.GetSymbol(); + symbol.IsValid() && symbol.GetName() != nullptr) + { + name = symbol.GetName(); + } + } + + if (name.empty()) + name = std::format("{:#x}", address); + + const uint32_t id = m_next_trace_id++; + m_instr_functions.emplace(address, id); + m_instr_names.emplace(id, std::move(name)); + return id; + } + + auto Debugger::span_label(uint32_t trace_id) const -> const char* + { + if (const auto entry = m_instr_names.find(trace_id); entry != m_instr_names.end()) + return entry->second.c_str(); + + const auto trace = std::ranges::find(m_traces, trace_id, &FunctionTrace::id); + if (trace != m_traces.end()) + return trace->function.c_str(); + + return "?"; + } + + auto Debugger::intern_named_function(std::string_view name) -> uint32_t + { + std::string key(name); + + if (const auto entry = m_sample_functions.find(key); entry != m_sample_functions.end()) + return entry->second; + + const uint32_t id = m_next_trace_id++; + m_sample_functions.emplace(key, id); + m_instr_names.emplace(id, std::move(key)); // shared id -> name store used by span_label + return id; + } + + auto Debugger::maybe_request_sample() -> void + { + // instrumentation, when present, is the exact source and wins + if (!m_sampling_enabled || m_instr_available) + return; + + if (!is_alive(m_session->process) || m_session->process.GetState() != lldb::eStateRunning) + return; + + if (m_sample_pending) + return; + + // update() runs once a frame, so this caps out near the frame rate; a + // short interval just means "as often as we can" + const auto now = std::chrono::steady_clock::now(); + if (now - m_sample_last < std::chrono::milliseconds(5)) + return; + + m_sample_last = now; + m_session->process.Stop(); + m_sample_pending = true; + } + + auto Debugger::take_sample_and_resume() -> bool + { + // a real breakpoint landing at the same time is a genuine stop, not a sample + const uint32_t threads = m_session->process.GetNumThreads(); + for (uint32_t index = 0; index < threads; ++index) + { + lldb::SBThread thread = m_session->process.GetThreadAtIndex(index); + if (thread.IsValid() && thread.GetStopReason() == lldb::eStopReasonBreakpoint) + return false; + } + + take_sample(); + m_session->process.Continue(); + return true; + } + + auto Debugger::take_sample() -> void + { + const double now = trace_now(); + const uint32_t threads = m_session->process.GetNumThreads(); + + std::vector<uint32_t> stack; + + for (uint32_t index = 0; index < threads; ++index) + { + lldb::SBThread thread = m_session->process.GetThreadAtIndex(index); + if (!thread.IsValid()) + continue; + + stack.clear(); + + // lldb numbers frames innermost-first, so walk from the top down to put + // the outermost call (main) at depth zero + const uint32_t frames = std::min(thread.GetNumFrames(), 64u); + for (int frame_index = static_cast<int>(frames) - 1; frame_index >= 0; --frame_index) + { + lldb::SBFrame frame = thread.GetFrameAtIndex(static_cast<uint32_t>(frame_index)); + if (frame.IsValid()) + stack.push_back(intern_named_function(name_of(frame))); + } + + fold_sample(thread.GetThreadID(), stack, now); + } + } + + auto Debugger::fold_sample(uint64_t thread_id, const std::vector<uint32_t>& stack, double now) -> void + { + std::vector<OpenSample>& open = m_sample_stacks[thread_id]; + + // how deep the new stack still matches the bars already open + size_t match = 0; + while (match < open.size() && match < stack.size() && open[match].trace_id == stack[match]) + ++match; + + // close every bar below the divergence point, deepest first + for (size_t depth = open.size(); depth-- > match;) + { + const size_t span = open[depth].span_index; + if (span < m_timeline.size()) + m_timeline[span].duration = now - m_timeline[span].start; + } + open.resize(match); + + // open a fresh bar for each newly seen frame + for (size_t depth = match; depth < stack.size(); ++depth) + { + size_t span_index = m_timeline.size(); + if (m_timeline.size() < MAX_TRACE_CALLS) + { + m_timeline.push_back(TimelineSpan{ stack[depth], thread_id, now, 0.0, + static_cast<uint32_t>(depth) }); + } + + open.push_back(OpenSample{ stack[depth], span_index }); + } + + // let still-open bars grow up to the current sample so they render live + for (const OpenSample& entry : open) + { + if (entry.span_index < m_timeline.size()) + m_timeline[entry.span_index].duration = now - m_timeline[entry.span_index].start; + } + } + auto Debugger::require_stopped() const -> Result<void> { if (!is_alive(m_session->process)) @@ -1495,11 +1947,20 @@ namespace Hsdbg case lldb::eStateStopped: if (m_state != TargetState::Stopped) { + // our own Stop() for a sample resolves here; clear the flag + // up front so a coincident breakpoint cannot strand it + const bool was_sampling = m_sample_pending; + m_sample_pending = false; + // a trace-only stop records the call and resumes without // ever surfacing to the user as a stop if (handle_trace_stop()) break; + // likewise a sampling stop grabs the stacks and resumes + if (was_sampling && take_sample_and_resume()) + break; + on_stopped(); } @@ -1611,6 +2072,10 @@ namespace Hsdbg set_state(TargetState::Stopped); + // the process is settled, so drain whatever the instrumentation buffer has + // gathered since the last stop into the timeline + read_instrumentation(); + ++m_stop_count; } diff --git a/src/debugger/debugger.h b/src/debugger/debugger.h index 7faa3a1..e2f4c62 100644 --- a/src/debugger/debugger.h +++ b/src/debugger/debugger.h @@ -6,12 +6,18 @@ #include <chrono> #include <cstdint> #include <filesystem> +#include <unordered_map> #include <memory> #include <span> #include <string> #include <string_view> #include <vector> +namespace lldb +{ + class SBThread; +} + namespace Hsdbg { class Debugger @@ -60,6 +66,22 @@ namespace Hsdbg auto clear_traces() -> void; auto traces() const -> std::span<const FunctionTrace> { return m_traces; } + // time-ordered call spans for the flame chart, spanning every traced thread + auto timeline() const -> std::span<const TimelineSpan> { return m_timeline; } + + // name behind a span's trace id, whether it came from a manual trace or + // from the automatic instrumentation buffer + auto span_label(uint32_t trace_id) const -> const char*; + + // whether the running target was built with the hsdbg trace runtime, so + // every function is being timed automatically + auto instrumentation_active() const -> bool { return m_instr_available; } + + // sampling profiler: no build changes, works on any binary. periodically + // pauses the running target and folds its call stacks into the timeline + auto set_sampling_enabled(bool enabled) -> void { m_sampling_enabled = enabled; } + auto sampling_enabled() const -> bool { return m_sampling_enabled; } + // inspection auto threads() const -> std::span<const Thread> { return m_threads; } auto call_stack() const -> std::span<const StackFrame> { return m_call_stack; } @@ -129,8 +151,58 @@ namespace Hsdbg // called for a stop that trace breakpoints took part in; returns true when // the stop was purely for tracing and the process was resumed auto handle_trace_stop() -> bool; + auto record_trace_entry(FunctionTrace& trace, lldb::SBThread& thread) -> void; + auto record_trace_return(lldb::SBThread& thread) -> void; auto trace_now() const -> double; + // automatic tracing: read the target's instrumentation ring buffer and turn + // its enter/exit records into timeline spans + struct InstrRecord; + auto resolve_instrumentation() -> void; + auto read_instrumentation() -> void; + auto apply_instr_record(const InstrRecord& record) -> void; + auto intern_instr_function(uint64_t address) -> uint32_t; + auto symbol_load_address(const char* name) -> uint64_t; + + // one instrumented call still on a thread's stack, waiting for its exit + struct InstrOpenCall + { + uint32_t trace_id = 0; + uint64_t start_ns = 0; + size_t span_index = 0; + }; + + // sampling profiler internals + auto maybe_request_sample() -> void; + auto take_sample_and_resume() -> bool; + auto take_sample() -> void; + auto fold_sample(uint64_t thread_id, const std::vector<uint32_t>& stack, double now) -> void; + auto intern_named_function(std::string_view name) -> uint32_t; + + // one frame currently open on a thread while folding samples into bars + struct OpenSample + { + uint32_t trace_id = 0; + size_t span_index = 0; + }; + + // a temporary breakpoint at a call's return address, so the matching exit + // can be timed; created lazily and torn down when the process restarts + auto ensure_return_breakpoint(uint64_t address) -> int32_t; + auto clear_return_breakpoints() -> void; + + // one in-flight call waiting for its return to be seen + struct PendingCall + { + uint32_t trace_id = 0; + uint64_t thread_id = 0; + uint64_t return_pc = 0; + uint64_t frame_sp = 0; + double start = 0.0; + size_t call_index = 0; + size_t span_index = 0; + }; + std::unique_ptr<Session> m_session; TargetState m_state = TargetState::NoTarget; @@ -144,6 +216,11 @@ namespace Hsdbg std::vector<FunctionTrace> m_traces; uint32_t m_next_trace_id = 1; + std::vector<PendingCall> m_pending_calls; + std::vector<TimelineSpan> m_timeline; + + // return address -> lldb breakpoint id, so call sites share one breakpoint + std::unordered_map<uint64_t, int32_t> m_return_breakpoints; std::vector<Thread> m_threads; std::vector<StackFrame> m_call_stack; @@ -162,5 +239,25 @@ namespace Hsdbg uint64_t m_resident_memory = 0; std::chrono::steady_clock::time_point m_trace_epoch = std::chrono::steady_clock::now(); + + // automatic instrumentation reader state, all rebuilt each run + bool m_instr_checked = false; + bool m_instr_available = false; + uint64_t m_instr_head_addr = 0; + uint64_t m_instr_records_addr = 0; + uint64_t m_instr_capacity = 0; + uint64_t m_instr_read_count = 0; + uint64_t m_instr_base_ns = 0; + bool m_instr_base_set = false; + std::unordered_map<uint64_t, uint32_t> m_instr_functions; // address -> trace id + std::unordered_map<uint32_t, std::string> m_instr_names; // trace id -> name + std::unordered_map<uint64_t, std::vector<InstrOpenCall>> m_instr_stacks; // thread -> open calls + + // sampling profiler state, also rebuilt each run + bool m_sampling_enabled = false; + bool m_sample_pending = false; + std::chrono::steady_clock::time_point m_sample_last{}; + std::unordered_map<std::string, uint32_t> m_sample_functions; // name -> trace id + std::unordered_map<uint64_t, std::vector<OpenSample>> m_sample_stacks; // thread -> open frames }; } diff --git a/src/debugger/types.h b/src/debugger/types.h index cda3dbd..0c4e95a 100644 --- a/src/debugger/types.h +++ b/src/debugger/types.h @@ -167,6 +167,17 @@ namespace Hsdbg double duration = 0.0; // seconds spent inside the call }; + // one call laid out for the flame chart: a horizontal bar whose x is the + // start time, width is the duration and row is how deeply it was nested + struct TimelineSpan + { + uint32_t trace_id = 0; // which traced function, also picks the colour + uint64_t thread_id = 0; + double start = 0.0; // seconds since tracing began + double duration = 0.0; // seconds; zero while the call is still running + uint32_t depth = 0; // 0 is the outermost call on its thread + }; + // a function the user asked to time. the debugger sets an internal breakpoint // on it that records each call without stopping the ui struct FunctionTrace @@ -174,7 +185,15 @@ namespace Hsdbg uint32_t id = 0; std::string function; int32_t entry_backend_id = 0; - uint64_t call_count = 0; + + uint64_t call_count = 0; // entries seen + uint64_t completed_count = 0; // calls whose return was matched + + // seconds, aggregated over completed calls + double total_time = 0.0; + double min_time = 0.0; + double max_time = 0.0; + std::vector<TraceCall> calls; }; } |
