diff --git a/.github/workflows/sbpf-dexter.yml b/.github/workflows/sbpf-dexter.yml index eb0351bf8cf1c..bcf91c280bec2 100644 --- a/.github/workflows/sbpf-dexter.yml +++ b/.github/workflows/sbpf-dexter.yml @@ -24,13 +24,15 @@ jobs: CCACHE_DIR: ${{ github.workspace }}/.ccache CCACHE_MAXSIZE: 2G steps: - - uses: actions/checkout@v6 + - name: Check out the LLVM and LLDB changes + uses: actions/checkout@v6 with: persist-credentials: false - - uses: actions/checkout@v6 + - name: Check out the coordinated SBPF runtime + uses: actions/checkout@v6 with: - repository: anza-xyz/sbpf - ref: main + repository: walnuthq/sbpf + ref: feat/improve-bt path: test-runtime persist-credentials: false - name: Install build dependencies @@ -59,12 +61,30 @@ jobs: -DLLVM_PARALLEL_LINK_JOBS=1 \ -DLLDB_ENABLE_PYTHON=ON -DPython3_EXECUTABLE=/usr/bin/python3 \ -DLLDB_ENABLE_LUA=OFF -DLLDB_ENABLE_CURSES=OFF \ - -DLLDB_INCLUDE_TESTS=OFF - cmake --build build --target lld lldb --parallel 2 - - name: Build the SBPF main interpreter with its debug server + -DLLDB_INCLUDE_TESTS=ON + cmake --build build --target lld lldb TargetTests ProcessGdbRemoteTests --parallel 2 + - name: Test frame identity and remote stack parsing + run: | + build/tools/lldb/unittests/Target/TargetTests --gtest_filter='StackIDTest.*' + build/tools/lldb/unittests/Process/gdb-remote/ProcessGdbRemoteTests --gtest_filter='SBPFCallStackTest.*' + - name: Test SBPF remote frames through the LLDB API + run: | + /usr/bin/python3 lldb/test/API/dotest.py \ + --executable "$PWD/build/bin/lldb" \ + --compiler /usr/bin/clang --cmake-build-type Release \ + --build-dir "$PWD/build/lldb-api-sbpf" \ + -p TestSBPFCallStack.py \ + lldb/test/API/functionalities/gdb_remote_client + - name: Verify the coordinated gdbstub branch + run: | + grep --fixed-strings \ + 'git+https://github.com/walnuthq/gdbstub?branch=feat%2Fsbpf-call-stack#' \ + test-runtime/cli/Cargo.lock + - name: Build the SBPF interpreter with its debug server run: | rustup toolchain install 1.98.1 --profile minimal cargo +1.98.1 build --locked --manifest-path test-runtime/cli/Cargo.toml + cargo +1.98.1 test --locked --manifest-path test-runtime/Cargo.toml --features debugger --test vm - name: Install the pinned SBPF Rust producer run: | curl --fail --location --retry 3 \ @@ -78,6 +98,7 @@ jobs: { git rev-parse HEAD git -C test-runtime rev-parse HEAD + sed -n '/name = "gdbstub"/,+3p' test-runtime/cli/Cargo.lock build/bin/ld.lld --version build/bin/lldb --version platform-tools/rust/bin/rustc -vV diff --git a/cross-project-tests/debuginfo-tests/sbpf/Inputs/recursive.rs b/cross-project-tests/debuginfo-tests/sbpf/Inputs/recursive.rs new file mode 100644 index 0000000000000..162e624c731c1 --- /dev/null +++ b/cross-project-tests/debuginfo-tests/sbpf/Inputs/recursive.rs @@ -0,0 +1,15 @@ +#![no_std] +#[inline(never)] +#[no_mangle] +pub extern "C" fn recurse(n: u64) -> u64 { + let n = unsafe { core::ptr::read_volatile(&n) }; + if n == 0 { + return 1; + } + let result = recurse(n - 1); + unsafe { core::ptr::read_volatile(&result) }.wrapping_add(n) +} +#[no_mangle] +pub extern "C" fn entrypoint() -> u64 { + recurse(3) +} diff --git a/cross-project-tests/debuginfo-tests/sbpf/Inputs/rust-recursive.dex b/cross-project-tests/debuginfo-tests/sbpf/Inputs/rust-recursive.dex new file mode 100644 index 0000000000000..020e42f940c0f --- /dev/null +++ b/cross-project-tests/debuginfo-tests/sbpf/Inputs/rust-recursive.dex @@ -0,0 +1,2 @@ +DexDeclareFile('recursive.rs') +DexExpectProgramState({'frames': [{'location': {'lineno': 6}}, {'location': {'lineno': 9}}, {'location': {'lineno': 9}}, {'location': {'lineno': 9}}, {'location': {'lineno': 14}}]}) diff --git a/cross-project-tests/debuginfo-tests/sbpf/Inputs/rust-steps.dex b/cross-project-tests/debuginfo-tests/sbpf/Inputs/rust-steps.dex index 0e51cfdcc2bb7..1104b2fac8438 100644 --- a/cross-project-tests/debuginfo-tests/sbpf/Inputs/rust-steps.dex +++ b/cross-project-tests/debuginfo-tests/sbpf/Inputs/rust-steps.dex @@ -1,4 +1,5 @@ DexDeclareFile('calls.rs') -DexExpectStepOrder(1, 5, on_line=14) -DexExpectStepOrder(2, 4, on_line=10) -DexExpectStepOrder(3, on_line=5) +# The two columns on line 5 are the volatile read and wrapping_add. +DexExpectStepOrder(1, 6, on_line=14) +DexExpectStepOrder(2, 5, on_line=10) +DexExpectStepOrder(3, 4, on_line=5) diff --git a/cross-project-tests/debuginfo-tests/sbpf/rust-callers.test b/cross-project-tests/debuginfo-tests/sbpf/rust-callers.test index b086e3ee96f79..1f16431a9c12e 100644 --- a/cross-project-tests/debuginfo-tests/sbpf/rust-callers.test +++ b/cross-project-tests/debuginfo-tests/sbpf/rust-callers.test @@ -1 +1 @@ -{"source": "calls.rs", "expectations": "rust-callers.dex", "xfail": "SBPF-UNWIND: saved interpreter caller frames are not exposed to LLDB"} +{"source": "calls.rs", "expectations": "rust-callers.dex"} diff --git a/cross-project-tests/debuginfo-tests/sbpf/rust-recursive.test b/cross-project-tests/debuginfo-tests/sbpf/rust-recursive.test new file mode 100644 index 0000000000000..c1dbbc780157c --- /dev/null +++ b/cross-project-tests/debuginfo-tests/sbpf/rust-recursive.test @@ -0,0 +1 @@ +{"source": "recursive.rs", "expectations": "rust-recursive.dex", "return_value": 7} diff --git a/lldb/docs/resources/lldbgdbremote.md b/lldb/docs/resources/lldbgdbremote.md index fdd9b057f0b4a..2140612e07744 100644 --- a/lldb/docs/resources/lldbgdbremote.md +++ b/lldb/docs/resources/lldbgdbremote.md @@ -2549,6 +2549,31 @@ read packet: $e0030100#b9 **Priority to Implement:** Only required for Wasm support. Necessary to show variables. +## SBPF Packets + +### qSBPFCallStack + +Get the SBPF interpreter call stack for the given thread id. The response is a +hex-encoded list without delimiters. Each frame contains a 64-bit PC followed +by the 64-bit value of SBPF register r10, both in little-endian byte order. +Frames are ordered from the active frame to the oldest caller. The active PC is +the current instruction; caller PCs are return addresses. + +``` +send packet: $qSBPFCallStack:1#79 +read packet: $2001000000000000002000000200000068010000000000000010000002000000#19 +``` + +**Priority to Implement:** Only required for interpreted SBPF support. +Necessary to show stack traces because return PCs are stored in host memory. + +The stack must include every physical frame through the oldest caller. LLDB +uses each frame's depth from that oldest caller as its identity across stops. +The frame pointer is the guest r10 register, not a DWARF CFA: dynamic-stack +versions can change it within a function or share it between recursive calls. +LLDB reports the CFA as unavailable when using this packet; register-based +DWARF expressions still use the actual guest registers. + ## "x" - Binary memory read > **Warning:** The format of this packet was decided before GDB 16 diff --git a/lldb/include/lldb/Target/StackID.h b/lldb/include/lldb/Target/StackID.h index 3f6a83b5e2fa5..f96a008c1ac12 100644 --- a/lldb/include/lldb/Target/StackID.h +++ b/lldb/include/lldb/Target/StackID.h @@ -10,6 +10,7 @@ #define LLDB_TARGET_STACKID_H #include "lldb/Core/AddressRange.h" +#include namespace lldb_private { @@ -20,7 +21,8 @@ class StackID { StackID() = default; explicit StackID(lldb::addr_t pc, lldb::addr_t cfa, - SymbolContextScope *symbol_scope, Process *process); + SymbolContextScope *symbol_scope, Process *process, + std::optional call_frame_depth = std::nullopt); ~StackID() = default; @@ -32,6 +34,10 @@ class StackID { lldb::addr_t GetCallFrameAddressWithoutMetadata() const { return m_cfa; } + std::optional GetCallFrameDepth() const { + return m_call_frame_depth; + } + SymbolContextScope *GetSymbolContextScope() const { return m_symbol_scope; } void SetSymbolContextScope(SymbolContextScope *symbol_scope) { @@ -41,11 +47,14 @@ class StackID { void Clear() { m_pc = LLDB_INVALID_ADDRESS; m_cfa = LLDB_INVALID_ADDRESS; + m_cfa_with_metadata = LLDB_INVALID_ADDRESS; + m_call_frame_depth.reset(); m_symbol_scope = nullptr; } bool IsValid() const { - return m_pc != LLDB_INVALID_ADDRESS || m_cfa != LLDB_INVALID_ADDRESS; + return m_call_frame_depth.has_value() || m_pc != LLDB_INVALID_ADDRESS || + m_cfa != LLDB_INVALID_ADDRESS; } void Dump(Stream *s); @@ -64,12 +73,16 @@ class StackID { /// The call frame address (stack pointer) value at the beginning of the /// function that uniquely identifies this frame (along with m_symbol_scope - /// below) + /// below), when no explicit call frame depth is available. lldb::addr_t m_cfa = LLDB_INVALID_ADDRESS; /// The cfa with metadata (i.e. prior to Process::FixAddress). lldb::addr_t m_cfa_with_metadata = LLDB_INVALID_ADDRESS; + /// Optional VM frame identity, counted from the oldest physical frame. + /// Greater depths are younger. This does not change CFA address semantics. + std::optional m_call_frame_depth; + /// If nullptr, there is no block or symbol for this frame. If not nullptr, /// this will either be the scope for the lexical block for the frame, or the /// scope for the symbol. Symbol context scopes are always be unique pointers diff --git a/lldb/include/lldb/Target/Unwind.h b/lldb/include/lldb/Target/Unwind.h index 105383ddae8a6..953023995adef 100644 --- a/lldb/include/lldb/Target/Unwind.h +++ b/lldb/include/lldb/Target/Unwind.h @@ -10,6 +10,7 @@ #define LLDB_TARGET_UNWIND_H #include +#include #include "lldb/lldb-private.h" @@ -58,10 +59,23 @@ class Unwind { return DoCreateRegisterContextForFrame(frame); } + /// An optional zero-based depth from the oldest physical frame. Unlike an + /// index from the current frame, this stays unchanged when callees are pushed + /// or popped. Virtual machines may use it to identify frames independently + /// of their guest stack addresses. It is not a canonical frame address. + std::optional GetCallFrameDepth(uint32_t frame_idx) { + std::lock_guard guard(m_unwind_mutex); + return DoGetCallFrameDepth(frame_idx); + } + Thread &GetThread() { return m_thread; } protected: // Classes that inherit from Unwind can see and modify these + virtual std::optional DoGetCallFrameDepth(uint32_t frame_idx) { + return std::nullopt; + } + virtual void DoClear() = 0; virtual uint32_t DoGetFrameCount() = 0; diff --git a/lldb/source/Plugins/Process/gdb-remote/CMakeLists.txt b/lldb/source/Plugins/Process/gdb-remote/CMakeLists.txt index 5c39384fa31b9..b69dc250e9c90 100644 --- a/lldb/source/Plugins/Process/gdb-remote/CMakeLists.txt +++ b/lldb/source/Plugins/Process/gdb-remote/CMakeLists.txt @@ -31,7 +31,10 @@ add_lldb_library(lldbPluginProcessGDBRemote PLUGIN GDBRemoteRegisterFallback.cpp ProcessGDBRemote.cpp ProcessGDBRemoteLog.cpp + RegisterContextSBPF.cpp + SBPFCallStack.cpp ThreadGDBRemote.cpp + UnwindSBPF.cpp LINK_COMPONENTS Support diff --git a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp index bae07245bdfa6..0c5ed96bdd8e4 100644 --- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp +++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp @@ -82,6 +82,7 @@ #include "Plugins/Process/Utility/StopInfoMachException.h" #include "ProcessGDBRemote.h" #include "ProcessGDBRemoteLog.h" +#include "SBPFCallStack.h" #include "ThreadGDBRemote.h" #include "lldb/Host/Host.h" #include "lldb/Utility/StringExtractorGDBRemote.h" @@ -220,6 +221,20 @@ void ProcessGDBRemote::DumpPluginHistory(Stream &s) { gdb_comm.DumpHistory(s); } +llvm::Expected> +ProcessGDBRemote::GetSBPFCallStack(lldb::tid_t tid) { + StreamString packet; + packet.Printf("qSBPFCallStack:%" PRIx64, tid); + + StringExtractorGDBRemote response; + if (m_gdb_comm.SendPacketAndWaitForResponse(packet.GetString(), response) != + GDBRemoteCommunication::PacketResult::Success) + return llvm::createStringError("failed to send qSBPFCallStack"); + if (!response.IsNormalResponse()) + return llvm::createStringError("failed to get qSBPFCallStack response"); + return ParseSBPFCallStack(response.GetStringRef()); +} + std::chrono::seconds ProcessGDBRemote::GetPacketTimeout() { return std::chrono::seconds(GetGlobalPluginProperties().GetPacketTimeout()); } diff --git a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h index db3c43284587b..0ed5683e4ed40 100644 --- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h +++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h @@ -36,6 +36,7 @@ #include "GDBRemoteCommunicationClient.h" #include "GDBRemoteRegisterContext.h" +#include "SBPFCallStack.h" #include "llvm/ADT/DenseMap.h" #include "llvm/ADT/StringMap.h" @@ -200,6 +201,8 @@ class ProcessGDBRemote : public Process, GDBRemoteCommunicationClient &GetGDBRemote() { return m_gdb_comm; } + llvm::Expected> GetSBPFCallStack(lldb::tid_t tid); + Status SendEventData(const char *data) override; // Override DidExit so we can disconnect from the remote GDB server diff --git a/lldb/source/Plugins/Process/gdb-remote/RegisterContextSBPF.cpp b/lldb/source/Plugins/Process/gdb-remote/RegisterContextSBPF.cpp new file mode 100644 index 0000000000000..d960a9672d8b9 --- /dev/null +++ b/lldb/source/Plugins/Process/gdb-remote/RegisterContextSBPF.cpp @@ -0,0 +1,74 @@ +//===-- RegisterContextSBPF.cpp ------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "RegisterContextSBPF.h" +#include "lldb/Utility/RegisterValue.h" +#include + +using namespace lldb; +using namespace lldb_private; +using namespace lldb_private::process_gdb_remote; + +RegisterContextSBPF::RegisterContextSBPF( + Thread &thread, uint32_t concrete_frame_idx, + GDBRemoteDynamicRegisterInfoSP register_info, lldb::addr_t pc, + lldb::addr_t frame_pointer) + : RegisterContext(thread, concrete_frame_idx), + m_register_info(std::move(register_info)), m_pc(pc), + m_frame_pointer(frame_pointer) {} + +void RegisterContextSBPF::InvalidateAllRegisters() {} + +size_t RegisterContextSBPF::GetRegisterCount() { + return m_register_info->GetNumRegisters(); +} + +const RegisterInfo *RegisterContextSBPF::GetRegisterInfoAtIndex(size_t reg) { + return m_register_info->GetRegisterInfoAtIndex(reg); +} + +size_t RegisterContextSBPF::GetRegisterSetCount() { + return m_register_info->GetNumRegisterSets(); +} + +const RegisterSet *RegisterContextSBPF::GetRegisterSet(size_t reg_set) { + return m_register_info->GetRegisterSet(reg_set); +} + +bool RegisterContextSBPF::ReadRegister(const RegisterInfo *reg_info, + RegisterValue &value) { + if (!reg_info) + return false; + + switch (reg_info->kinds[eRegisterKindGeneric]) { + case LLDB_REGNUM_GENERIC_PC: + return value.SetUInt(m_pc, reg_info->byte_size); + case LLDB_REGNUM_GENERIC_FP: + return value.SetUInt(m_frame_pointer, reg_info->byte_size); + default: + return false; + } +} + +bool RegisterContextSBPF::WriteRegister(const RegisterInfo *, + const RegisterValue &) { + return false; +} + +bool RegisterContextSBPF::ReadAllRegisterValues(WritableDataBufferSP &) { + return false; +} + +bool RegisterContextSBPF::WriteAllRegisterValues(const DataBufferSP &) { + return false; +} + +uint32_t RegisterContextSBPF::ConvertRegisterKindToRegisterNumber( + lldb::RegisterKind kind, uint32_t num) { + return m_register_info->ConvertRegisterKindToRegisterNumber(kind, num); +} diff --git a/lldb/source/Plugins/Process/gdb-remote/RegisterContextSBPF.h b/lldb/source/Plugins/Process/gdb-remote/RegisterContextSBPF.h new file mode 100644 index 0000000000000..9fc54844799bc --- /dev/null +++ b/lldb/source/Plugins/Process/gdb-remote/RegisterContextSBPF.h @@ -0,0 +1,47 @@ +//===-- RegisterContextSBPF.h ----------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLDB_SOURCE_PLUGINS_PROCESS_GDB_REMOTE_REGISTERCONTEXTSBPF_H +#define LLDB_SOURCE_PLUGINS_PROCESS_GDB_REMOTE_REGISTERCONTEXTSBPF_H + +#include "GDBRemoteRegisterContext.h" + +namespace lldb_private::process_gdb_remote { + +/// Register context for an interpreter-owned SBPF caller frame. The runtime +/// supplies the frame's PC and r10 value; other historical register values are +/// unavailable. +class RegisterContextSBPF : public RegisterContext { +public: + RegisterContextSBPF(Thread &thread, uint32_t concrete_frame_idx, + GDBRemoteDynamicRegisterInfoSP register_info, + lldb::addr_t pc, lldb::addr_t frame_pointer); + + void InvalidateAllRegisters() override; + size_t GetRegisterCount() override; + const RegisterInfo *GetRegisterInfoAtIndex(size_t reg) override; + size_t GetRegisterSetCount() override; + const RegisterSet *GetRegisterSet(size_t reg_set) override; + bool ReadRegister(const RegisterInfo *reg_info, + RegisterValue &value) override; + bool WriteRegister(const RegisterInfo *reg_info, + const RegisterValue &value) override; + bool ReadAllRegisterValues(lldb::WritableDataBufferSP &data_sp) override; + bool WriteAllRegisterValues(const lldb::DataBufferSP &data_sp) override; + uint32_t ConvertRegisterKindToRegisterNumber(lldb::RegisterKind kind, + uint32_t num) override; + +private: + GDBRemoteDynamicRegisterInfoSP m_register_info; + lldb::addr_t m_pc; + lldb::addr_t m_frame_pointer; +}; + +} // namespace lldb_private::process_gdb_remote + +#endif diff --git a/lldb/source/Plugins/Process/gdb-remote/SBPFCallStack.cpp b/lldb/source/Plugins/Process/gdb-remote/SBPFCallStack.cpp new file mode 100644 index 0000000000000..8fa9656c26ed8 --- /dev/null +++ b/lldb/source/Plugins/Process/gdb-remote/SBPFCallStack.cpp @@ -0,0 +1,32 @@ +//===-- SBPFCallStack.cpp -------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "SBPFCallStack.h" +#include "llvm/ADT/StringExtras.h" +#include "llvm/Support/Endian.h" + +using namespace lldb_private::process_gdb_remote; + +llvm::Expected> +lldb_private::process_gdb_remote::ParseSBPFCallStack(llvm::StringRef response) { + constexpr size_t frame_size = 2 * sizeof(uint64_t); + std::string bytes; + if (response.size() % 2 != 0 || !llvm::tryGetFromHex(response, bytes) || + bytes.empty() || + bytes.size() % frame_size != 0) + return llvm::createStringError("invalid qSBPFCallStack response"); + + std::vector frames; + for (size_t offset = 0; offset < bytes.size(); offset += frame_size) { + const char *frame = bytes.data() + offset; + frames.push_back( + {llvm::support::endian::read64le(frame), + llvm::support::endian::read64le(frame + sizeof(uint64_t))}); + } + return frames; +} diff --git a/lldb/source/Plugins/Process/gdb-remote/SBPFCallStack.h b/lldb/source/Plugins/Process/gdb-remote/SBPFCallStack.h new file mode 100644 index 0000000000000..f1e15ec927521 --- /dev/null +++ b/lldb/source/Plugins/Process/gdb-remote/SBPFCallStack.h @@ -0,0 +1,33 @@ +//===-- SBPFCallStack.h ----------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLDB_SOURCE_PLUGINS_PROCESS_GDB_REMOTE_SBPFCALLSTACK_H +#define LLDB_SOURCE_PLUGINS_PROCESS_GDB_REMOTE_SBPFCALLSTACK_H + +#include "lldb/lldb-types.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Error.h" +#include + +namespace lldb_private::process_gdb_remote { + +struct SBPFCallFrame { + lldb::addr_t pc; + lldb::addr_t frame_pointer; +}; + +/// Parse the response to qSBPFCallStack. Each frame is a pair of little-endian +/// 64-bit PC and frame-pointer values, hex encoded without delimiters. Frames +/// are ordered from the active frame to the oldest caller. Caller PCs are +/// return PCs and use LLDB's normal non-zero-frame address adjustment. +llvm::Expected> +ParseSBPFCallStack(llvm::StringRef response); + +} // namespace lldb_private::process_gdb_remote + +#endif diff --git a/lldb/source/Plugins/Process/gdb-remote/ThreadGDBRemote.cpp b/lldb/source/Plugins/Process/gdb-remote/ThreadGDBRemote.cpp index 3d23c074c1be3..afc0b82ee8c1c 100644 --- a/lldb/source/Plugins/Process/gdb-remote/ThreadGDBRemote.cpp +++ b/lldb/source/Plugins/Process/gdb-remote/ThreadGDBRemote.cpp @@ -24,6 +24,7 @@ #include "ProcessGDBRemote.h" #include "ProcessGDBRemoteLog.h" +#include "UnwindSBPF.h" #include @@ -60,6 +61,27 @@ ThreadGDBRemote::~ThreadGDBRemote() { DestroyThread(); } +llvm::Expected> ThreadGDBRemote::GetSBPFCallStack() { + ProcessSP process_sp = GetProcess(); + if (!process_sp) + return llvm::createStringError("no process"); + return static_cast(process_sp.get()) + ->GetSBPFCallStack(GetID()); +} + +Unwind &ThreadGDBRemote::GetUnwinder() { + if (m_unwinder_up) + return *m_unwinder_up; + + TargetSP target_sp = CalculateTarget(); + if (!target_sp || + target_sp->GetArchitecture().GetMachine() != llvm::Triple::sbf) + return Thread::GetUnwinder(); + + m_unwinder_up = std::make_unique(*this); + return *m_unwinder_up; +} + const char *ThreadGDBRemote::GetName() { if (m_thread_name.empty()) return nullptr; diff --git a/lldb/source/Plugins/Process/gdb-remote/ThreadGDBRemote.h b/lldb/source/Plugins/Process/gdb-remote/ThreadGDBRemote.h index 5bc90a3dedceb..5fe3ae93008ff 100644 --- a/lldb/source/Plugins/Process/gdb-remote/ThreadGDBRemote.h +++ b/lldb/source/Plugins/Process/gdb-remote/ThreadGDBRemote.h @@ -15,6 +15,7 @@ #include "lldb/Utility/StructuredData.h" #include "GDBRemoteRegisterContext.h" +#include "SBPFCallStack.h" class StringExtractor; @@ -53,6 +54,14 @@ class ThreadGDBRemote : public Thread { lldb::RegisterContextSP GetRegisterContext() override; + Unwind &GetUnwinder() override; + + llvm::Expected> GetSBPFCallStack(); + + GDBRemoteDynamicRegisterInfoSP GetDynamicRegisterInfo() const { + return m_reg_info_sp; + } + lldb::RegisterContextSP CreateRegisterContextForFrame(StackFrame *frame) override; diff --git a/lldb/source/Plugins/Process/gdb-remote/UnwindSBPF.cpp b/lldb/source/Plugins/Process/gdb-remote/UnwindSBPF.cpp new file mode 100644 index 0000000000000..d96c970aa0b3a --- /dev/null +++ b/lldb/source/Plugins/Process/gdb-remote/UnwindSBPF.cpp @@ -0,0 +1,88 @@ +//===-- UnwindSBPF.cpp ----------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "UnwindSBPF.h" +#include "RegisterContextSBPF.h" +#include "ThreadGDBRemote.h" +#include "lldb/Target/Process.h" +#include "lldb/Target/StackFrame.h" +#include "lldb/Target/UnwindLLDB.h" +#include "lldb/Utility/LLDBLog.h" +#include "lldb/Utility/Log.h" + +using namespace lldb; +using namespace lldb_private; +using namespace lldb_private::process_gdb_remote; + +void UnwindSBPF::DoClear() { + m_frames.clear(); + m_unwind_complete = false; + m_fallback.reset(); +} + +uint32_t UnwindSBPF::DoGetFrameCount() { + if (m_fallback) + return m_fallback->GetFrameCount(); + if (m_unwind_complete) + return m_frames.size(); + + m_unwind_complete = true; + ThreadGDBRemote &thread = static_cast(GetThread()); + llvm::Expected> frames = thread.GetSBPFCallStack(); + if (!frames) { + LLDB_LOG_ERROR(GetLog(LLDBLog::Unwind), frames.takeError(), + "SBPF remote unwinding unavailable: {0}"); + m_fallback = std::make_unique(GetThread()); + return m_fallback->GetFrameCount(); + } + m_frames = std::move(*frames); + return m_frames.size(); +} + +bool UnwindSBPF::DoGetFrameInfoAtIndex(uint32_t frame_idx, lldb::addr_t &cfa, + lldb::addr_t &pc, + bool &behaves_like_zeroth_frame) { + if (!m_unwind_complete) + DoGetFrameCount(); + if (m_fallback) + return m_fallback->GetFrameInfoAtIndex(frame_idx, cfa, pc, + behaves_like_zeroth_frame); + if (frame_idx >= m_frames.size()) + return false; + + behaves_like_zeroth_frame = frame_idx == 0; + pc = m_frames[frame_idx].pc; + // The remote packet contains r10, not DWARF call frame information. Do not + // expose a guest frame pointer (or an ordering key) as a fabricated CFA. + cfa = LLDB_INVALID_ADDRESS; + return true; +} + +std::optional UnwindSBPF::DoGetCallFrameDepth(uint32_t frame_idx) { + if (!m_unwind_complete) + DoGetFrameCount(); + if (m_fallback || frame_idx >= m_frames.size()) + return std::nullopt; + return static_cast(m_frames.size() - frame_idx - 1); +} + +lldb::RegisterContextSP +UnwindSBPF::DoCreateRegisterContextForFrame(StackFrame *frame) { + uint32_t frame_idx = frame->GetConcreteFrameIndex(); + if (!m_unwind_complete) + DoGetFrameCount(); + if (m_fallback) + return m_fallback->CreateRegisterContextForFrame(frame); + if (frame_idx >= m_frames.size()) + return {}; + + ThreadGDBRemote &thread = static_cast(GetThread()); + return std::make_shared( + thread, frame_idx, thread.GetDynamicRegisterInfo(), + m_frames[frame_idx].pc, m_frames[frame_idx].frame_pointer); +} diff --git a/lldb/source/Plugins/Process/gdb-remote/UnwindSBPF.h b/lldb/source/Plugins/Process/gdb-remote/UnwindSBPF.h new file mode 100644 index 0000000000000..d52738056b585 --- /dev/null +++ b/lldb/source/Plugins/Process/gdb-remote/UnwindSBPF.h @@ -0,0 +1,40 @@ +//===-- UnwindSBPF.h ------------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLDB_SOURCE_PLUGINS_PROCESS_GDB_REMOTE_UNWINDSBPF_H +#define LLDB_SOURCE_PLUGINS_PROCESS_GDB_REMOTE_UNWINDSBPF_H + +#include "SBPFCallStack.h" +#include "lldb/Target/Unwind.h" +#include + +namespace lldb_private::process_gdb_remote { + +class UnwindSBPF : public Unwind { +public: + UnwindSBPF(Thread &thread) : Unwind(thread) {} + +protected: + std::optional DoGetCallFrameDepth(uint32_t frame_idx) override; + void DoClear() override; + uint32_t DoGetFrameCount() override; + bool DoGetFrameInfoAtIndex(uint32_t frame_idx, lldb::addr_t &cfa, + lldb::addr_t &pc, + bool &behaves_like_zeroth_frame) override; + lldb::RegisterContextSP + DoCreateRegisterContextForFrame(StackFrame *frame) override; + +private: + std::vector m_frames; + std::unique_ptr m_fallback; + bool m_unwind_complete = false; +}; + +} // namespace lldb_private::process_gdb_remote + +#endif diff --git a/lldb/source/Target/StackFrame.cpp b/lldb/source/Target/StackFrame.cpp index 340607e14abed..ba7eb741ac967 100644 --- a/lldb/source/Target/StackFrame.cpp +++ b/lldb/source/Target/StackFrame.cpp @@ -28,6 +28,7 @@ #include "lldb/Target/StackFrameRecognizer.h" #include "lldb/Target/Target.h" #include "lldb/Target/Thread.h" +#include "lldb/Target/Unwind.h" #include "lldb/Utility/LLDBLog.h" #include "lldb/Utility/Log.h" #include "lldb/Utility/RegisterValue.h" @@ -64,7 +65,10 @@ StackFrame::StackFrame(const ThreadSP &thread_sp, user_id_t frame_idx, const SymbolContext *sc_ptr) : m_thread_wp(thread_sp), m_frame_index(frame_idx), m_concrete_frame_index(unwind_frame_index), m_reg_context_sp(), - m_id(pc, cfa, nullptr, thread_sp->GetProcess().get()), + m_id(pc, cfa, nullptr, thread_sp->GetProcess().get(), + kind == Kind::Regular && !artificial + ? thread_sp->GetUnwinder().GetCallFrameDepth(unwind_frame_index) + : std::nullopt), m_frame_code_addr(pc), m_sc(), m_flags(), m_frame_base(), m_frame_base_error(), m_cfa_is_valid(cfa_is_valid), m_stack_frame_kind(kind), m_artificial(artificial), @@ -92,7 +96,8 @@ StackFrame::StackFrame(const ThreadSP &thread_sp, user_id_t frame_idx, : m_thread_wp(thread_sp), m_frame_index(frame_idx), m_concrete_frame_index(unwind_frame_index), m_reg_context_sp(reg_context_sp), - m_id(pc, cfa, nullptr, thread_sp->GetProcess().get()), + m_id(pc, cfa, nullptr, thread_sp->GetProcess().get(), + thread_sp->GetUnwinder().GetCallFrameDepth(unwind_frame_index)), m_frame_code_addr(pc), m_sc(), m_flags(), m_frame_base(), m_frame_base_error(), m_cfa_is_valid(true), m_stack_frame_kind(StackFrame::Kind::Regular), m_artificial(false), @@ -120,7 +125,8 @@ StackFrame::StackFrame(const ThreadSP &thread_sp, user_id_t frame_idx, m_concrete_frame_index(unwind_frame_index), m_reg_context_sp(reg_context_sp), m_id(pc_addr.GetLoadAddress(thread_sp->CalculateTarget().get()), cfa, - nullptr, thread_sp->GetProcess().get()), + nullptr, thread_sp->GetProcess().get(), + thread_sp->GetUnwinder().GetCallFrameDepth(unwind_frame_index)), m_frame_code_addr(pc_addr), m_sc(), m_flags(), m_frame_base(), m_frame_base_error(), m_cfa_is_valid(true), m_stack_frame_kind(StackFrame::Kind::Regular), m_artificial(false), diff --git a/lldb/source/Target/StackID.cpp b/lldb/source/Target/StackID.cpp index 137c776a84d2f..7a5f7d1767270 100644 --- a/lldb/source/Target/StackID.cpp +++ b/lldb/source/Target/StackID.cpp @@ -16,9 +16,10 @@ using namespace lldb_private; StackID::StackID(lldb::addr_t pc, lldb::addr_t cfa, - SymbolContextScope *symbol_scope, Process *process) + SymbolContextScope *symbol_scope, Process *process, + std::optional call_frame_depth) : m_pc(pc), m_cfa(cfa), m_cfa_with_metadata(cfa), - m_symbol_scope(symbol_scope) { + m_call_frame_depth(call_frame_depth), m_symbol_scope(symbol_scope) { if (process) { m_pc = process->FixCodeAddress(m_pc); m_cfa = process->FixDataAddress(m_cfa); @@ -38,6 +39,8 @@ void StackID::Dump(Stream *s) { s->Printf("StackID (pc = 0x%16.16" PRIx64 ", cfa = 0x%16.16" PRIx64 ", symbol_scope = %p", m_pc, m_cfa, static_cast(m_symbol_scope)); + if (m_call_frame_depth) + s->Printf(", depth = %" PRIu32, *m_call_frame_depth); if (m_symbol_scope) { SymbolContext sc; @@ -51,16 +54,20 @@ void StackID::Dump(Stream *s) { } bool lldb_private::operator==(const StackID &lhs, const StackID &rhs) { - if (lhs.GetCallFrameAddressWithoutMetadata() != - rhs.GetCallFrameAddressWithoutMetadata()) + if (lhs.GetCallFrameDepth() || rhs.GetCallFrameDepth()) { + if (lhs.GetCallFrameDepth() != rhs.GetCallFrameDepth()) + return false; + } else if (lhs.GetCallFrameAddressWithoutMetadata() != + rhs.GetCallFrameAddressWithoutMetadata()) { return false; + } SymbolContextScope *lhs_scope = lhs.GetSymbolContextScope(); SymbolContextScope *rhs_scope = rhs.GetSymbolContextScope(); // Only compare the PC values if both symbol context scopes are nullptr if (lhs_scope == nullptr && rhs_scope == nullptr) - return lhs.GetPC() == rhs.GetPC(); + return lhs.GetCallFrameDepth().has_value() || lhs.GetPC() == rhs.GetPC(); return lhs_scope == rhs_scope; } @@ -70,20 +77,26 @@ bool lldb_private::operator!=(const StackID &lhs, const StackID &rhs) { } bool lldb_private::operator<(const StackID &lhs, const StackID &rhs) { - const lldb::addr_t lhs_cfa = lhs.GetCallFrameAddressWithoutMetadata(); - const lldb::addr_t rhs_cfa = rhs.GetCallFrameAddressWithoutMetadata(); - - // FIXME: We are assuming that the stacks grow downward in memory. That's not - // necessary, but true on - // all the machines we care about at present. If this changes, we'll have to - // deal with that. The ABI is the agent who knows this ordering, but the - // StackID has no access to the ABI. The most straightforward way to handle - // this is to add a "m_grows_downward" bool to the StackID, and set it in the - // constructor. But I'm not going to waste a bool per StackID on this till we - // need it. - - if (lhs_cfa != rhs_cfa) - return lhs_cfa < rhs_cfa; + // A depth identifies physical VM frames even when they share a guest stack + // pointer. Keep the existing scope comparison for inline frames at one depth. + if (lhs.GetCallFrameDepth() || rhs.GetCallFrameDepth()) { + if (lhs.GetCallFrameDepth() != rhs.GetCallFrameDepth()) + return lhs.GetCallFrameDepth() > rhs.GetCallFrameDepth(); + } else { + const lldb::addr_t lhs_cfa = lhs.GetCallFrameAddressWithoutMetadata(); + const lldb::addr_t rhs_cfa = rhs.GetCallFrameAddressWithoutMetadata(); + + // FIXME: We are assuming that the stacks grow downward in memory. That's + // not necessary, but true on all the machines we care about at present. If + // this changes, we'll have to deal with that. The ABI is the agent who + // knows this ordering, but the StackID has no access to the ABI. The most + // straightforward way to handle this is to add a "m_grows_downward" bool to + // the StackID, and set it in the constructor. But I'm not going to waste a + // bool per StackID on this till we need it. + + if (lhs_cfa != rhs_cfa) + return lhs_cfa < rhs_cfa; + } SymbolContextScope *lhs_scope = lhs.GetSymbolContextScope(); SymbolContextScope *rhs_scope = rhs.GetSymbolContextScope(); diff --git a/lldb/test/API/functionalities/gdb_remote_client/TestSBPFCallStack.py b/lldb/test/API/functionalities/gdb_remote_client/TestSBPFCallStack.py new file mode 100644 index 0000000000000..c208bd4bfb214 --- /dev/null +++ b/lldb/test/API/functionalities/gdb_remote_client/TestSBPFCallStack.py @@ -0,0 +1,104 @@ +import struct + +import lldb +from lldbsuite.test.decorators import skipIfXmlSupportMissing +from lldbsuite.test.gdbclientutils import MockGDBServerResponder +from lldbsuite.test.lldbgdbclient import GDBRemoteTestBase + + +class SBPFResponder(MockGDBServerResponder): + def __init__(self, frame_pointers): + super().__init__() + # Identical PCs model recursion through the same call site. + self.frames = [(0x100, fp) for fp in frame_pointers] + self.frames[0] = (0x108, frame_pointers[0]) + + def _respond_impl(self, packet): + # Step past the instruction at a recursive return address before + # continuing to the next return. + if packet == "s": + pc, fp = self.frames[0] + self.frames[0] = (pc + 8, fp) + return self.haltReason() + return super()._respond_impl(packet) + + def qC(self): + return "QC1" + + def qfThreadInfo(self): + return "m1" + + def haltReason(self): + return "T05thread:1;" + + def qXferRead(self, obj, annex, offset, length): + if obj != "features" or annex != "target.xml": + return None, False + registers = [] + for index in range(12): + generic = {10: ' generic="fp"', 11: ' generic="pc"'}.get(index, "") + registers.append( + f'' + ) + xml = ( + 'sbpf' + '' + + "".join(registers) + + "" + ) + return xml[offset : offset + length], offset + length < len(xml) + + def readRegister(self, register): + value = {10: self.frames[0][1], 11: self.frames[0][0]}.get(register, 0) + return struct.pack(" 1: + thread.StepOut() + self.assertEqual(process.GetState(), lldb.eStateStopped) + self.assertPacketLogReceived(["qSBPFCallStack:1"]) + + @skipIfXmlSupportMissing + def test_fixed_frames(self): + self.check_frames([0x200006000, 0x200004000, 0x200002000]) + + @skipIfXmlSupportMissing + def test_dynamic_frames(self): + self.check_frames([0x200003F80, 0x200003FC0, 0x200004000]) + + @skipIfXmlSupportMissing + def test_recursive_frames_with_same_pointer(self): + self.check_frames([0x200004000] * 3) diff --git a/lldb/unittests/Process/gdb-remote/CMakeLists.txt b/lldb/unittests/Process/gdb-remote/CMakeLists.txt index 50a97e5cebaa9..686706ed98a8d 100644 --- a/lldb/unittests/Process/gdb-remote/CMakeLists.txt +++ b/lldb/unittests/Process/gdb-remote/CMakeLists.txt @@ -5,6 +5,7 @@ add_lldb_unittest(ProcessGdbRemoteTests GDBRemoteCommunicationServerTest.cpp GDBRemoteCommunicationTest.cpp GDBRemoteTestUtils.cpp + SBPFCallStackTest.cpp LINK_COMPONENTS Support diff --git a/lldb/unittests/Process/gdb-remote/SBPFCallStackTest.cpp b/lldb/unittests/Process/gdb-remote/SBPFCallStackTest.cpp new file mode 100644 index 0000000000000..0a87d4c4b495e --- /dev/null +++ b/lldb/unittests/Process/gdb-remote/SBPFCallStackTest.cpp @@ -0,0 +1,50 @@ +//===-- SBPFCallStackTest.cpp ---------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "Plugins/Process/gdb-remote/SBPFCallStack.h" +#include "gtest/gtest.h" + +using namespace lldb_private::process_gdb_remote; + +TEST(SBPFCallStackTest, ParsesCurrentAndCallerFrames) { + auto frames = ParseSBPFCallStack("20010000000000000020000002000000" + "68010000000000000010000002000000" + "80010000000000000000000002000000"); + ASSERT_TRUE(static_cast(frames)); + ASSERT_EQ(frames->size(), 3u); + EXPECT_EQ((*frames)[0].pc, 0x120u); + EXPECT_EQ((*frames)[0].frame_pointer, 0x200002000u); + EXPECT_EQ((*frames)[1].pc, 0x168u); + EXPECT_EQ((*frames)[1].frame_pointer, 0x200001000u); + EXPECT_EQ((*frames)[2].pc, 0x180u); + EXPECT_EQ((*frames)[2].frame_pointer, 0x200000000u); +} + +TEST(SBPFCallStackTest, RejectsInvalidHex) { + auto frames = ParseSBPFCallStack("not-hex"); + ASSERT_FALSE(static_cast(frames)); + llvm::consumeError(frames.takeError()); +} + +TEST(SBPFCallStackTest, RejectsIncompleteFrame) { + auto frames = ParseSBPFCallStack("2001000000000000"); + ASSERT_FALSE(static_cast(frames)); + llvm::consumeError(frames.takeError()); +} + +TEST(SBPFCallStackTest, RejectsOddHexLength) { + auto frames = ParseSBPFCallStack("00112233445566778899aabbccddeef"); + ASSERT_FALSE(static_cast(frames)); + llvm::consumeError(frames.takeError()); +} + +TEST(SBPFCallStackTest, RejectsEmptyStack) { + auto frames = ParseSBPFCallStack(""); + ASSERT_FALSE(static_cast(frames)); + llvm::consumeError(frames.takeError()); +} diff --git a/lldb/unittests/Target/CMakeLists.txt b/lldb/unittests/Target/CMakeLists.txt index bf08a8f015ba0..28be9b6432e34 100644 --- a/lldb/unittests/Target/CMakeLists.txt +++ b/lldb/unittests/Target/CMakeLists.txt @@ -12,6 +12,7 @@ add_lldb_unittest(TargetTests RegisterFlagsTest.cpp RemoteAwarePlatformTest.cpp ScratchTypeSystemTest.cpp + StackIDTest.cpp StackFrameRecognizerTest.cpp SummaryStatisticsTest.cpp FindFileTest.cpp diff --git a/lldb/unittests/Target/StackIDTest.cpp b/lldb/unittests/Target/StackIDTest.cpp new file mode 100644 index 0000000000000..0385e2b3c80d3 --- /dev/null +++ b/lldb/unittests/Target/StackIDTest.cpp @@ -0,0 +1,62 @@ +//===-- StackIDTest.cpp +//----------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "lldb/Target/StackID.h" +#include "gtest/gtest.h" + +using namespace lldb_private; + +TEST(StackIDTest, NativeFramesRetainAddressOrdering) { + StackID callee(0x100, 0x2000, nullptr, nullptr); + StackID caller(0x200, 0x3000, nullptr, nullptr); + EXPECT_LT(callee, caller); + EXPECT_NE(callee, caller); +} + +TEST(StackIDTest, DepthOrdersFramesRegardlessOfStackDirection) { + for (lldb::addr_t callee_cfa : {0x2000, 0x3000, 0x4000}) { + StackID callee(0x100, callee_cfa, nullptr, nullptr, 2); + StackID caller(0x100, 0x3000, nullptr, nullptr, 1); + EXPECT_LT(callee, caller); + EXPECT_FALSE(caller < callee); + EXPECT_NE(callee, caller); + EXPECT_EQ(callee.GetCallFrameAddressWithoutMetadata(), callee_cfa); + } +} + +TEST(StackIDTest, RecursiveFramesWithoutCFARemainDistinct) { + StackID callee(0x100, LLDB_INVALID_ADDRESS, nullptr, nullptr, 2); + StackID caller(0x100, LLDB_INVALID_ADDRESS, nullptr, nullptr, 1); + EXPECT_NE(callee, caller); + EXPECT_LT(callee, caller); + EXPECT_EQ(callee.GetCallFrameAddressWithMetadata(), LLDB_INVALID_ADDRESS); + EXPECT_EQ(callee.GetCallFrameAddressWithoutMetadata(), LLDB_INVALID_ADDRESS); +} + +TEST(StackIDTest, IdentitySurvivesGuestStackAndPCChanges) { + StackID before(0x100, 0x3000, nullptr, nullptr, 1); + StackID after(0x108, 0x2fc0, nullptr, nullptr, 1); + EXPECT_EQ(before, after); + EXPECT_FALSE(before < after); + EXPECT_FALSE(after < before); +} + +TEST(StackIDTest, DepthDoesNotMatchFallbackAddressIdentity) { + StackID remote(0x100, LLDB_INVALID_ADDRESS, nullptr, nullptr, 0); + StackID fallback(0x100, LLDB_INVALID_ADDRESS, nullptr, nullptr); + EXPECT_NE(remote, fallback); +} + +TEST(StackIDTest, ClearRemovesDepthAndAddressMetadata) { + StackID id(0x100, 0x3000, nullptr, nullptr, 0); + id.Clear(); + EXPECT_FALSE(id.IsValid()); + EXPECT_FALSE(id.GetCallFrameDepth()); + EXPECT_EQ(id.GetCallFrameAddressWithMetadata(), LLDB_INVALID_ADDRESS); +}