[Lldb-commits] [lldb] [lldb][Windows] Fix x86_64 default unwind plan (PR #210076)
Charles Zablit via lldb-commits
lldb-commits at lists.llvm.org
Thu Jul 16 07:50:57 PDT 2026
https://github.com/charles-zablit created https://github.com/llvm/llvm-project/pull/210076
`ABIWindows_x86_64::CreateDefaultUnwindPlan()` uses a "CFA = rbp + 16" as a placeholder, however, `Windows-x86_64` does not use rbp as a frame pointer. With this bogus rbp based CFA, the unwinder could not produce the caller frame, so stepping out of such a function fails with "Could not create return address breakpoint".
This patch uses the `Windows-x86_64` correct rule for the fallback: [the return address is at the top of the stack](https://learn.microsoft.com/en-us/cpp/build/x64-software-conventions?view=msvc-170), so `CFA = rsp + 8` and `pc = [CFA - 8]`.
This patch also ports `lang/c/trampoline_stepping` from Swiftlang.
>From 59f6e466cd01831e9ce2bafb5db427eba8a1c9b8 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Thu, 16 Jul 2026 15:43:10 +0100
Subject: [PATCH] [lldb][Windows] Fix x86_64 default unwind plan; enable
trampoline_stepping
---
.../Plugins/ABI/X86/ABIWindows_x86_64.cpp | 10 +-
.../API/lang/c/trampoline_stepping/Makefile | 3 +
.../TestTrampolineStepping.py | 100 ++++++++++++++++++
.../API/lang/c/trampoline_stepping/main.c | 52 +++++++++
4 files changed, 157 insertions(+), 8 deletions(-)
create mode 100644 lldb/test/API/lang/c/trampoline_stepping/Makefile
create mode 100644 lldb/test/API/lang/c/trampoline_stepping/TestTrampolineStepping.py
create mode 100644 lldb/test/API/lang/c/trampoline_stepping/main.c
diff --git a/lldb/source/Plugins/ABI/X86/ABIWindows_x86_64.cpp b/lldb/source/Plugins/ABI/X86/ABIWindows_x86_64.cpp
index 079b22a307602..be3c83880bd60 100644
--- a/lldb/source/Plugins/ABI/X86/ABIWindows_x86_64.cpp
+++ b/lldb/source/Plugins/ABI/X86/ABIWindows_x86_64.cpp
@@ -751,21 +751,15 @@ UnwindPlanSP ABIWindows_x86_64::CreateFunctionEntryUnwindPlan() {
// Windows-x86_64 doesn't use %rbp
// No available Unwind information for Windows-x86_64 (section .pdata)
-// Let's use SysV-x86_64 one for now
UnwindPlanSP ABIWindows_x86_64::CreateDefaultUnwindPlan() {
- uint32_t fp_reg_num = dwarf_rbp;
uint32_t sp_reg_num = dwarf_rsp;
uint32_t pc_reg_num = dwarf_rip;
UnwindPlan::Row row;
-
- const int32_t ptr_size = 8;
- row.GetCFAValue().SetIsRegisterPlusOffset(dwarf_rbp, 2 * ptr_size);
row.SetOffset(0);
row.SetUnspecifiedRegistersAreUndefined(true);
-
- row.SetRegisterLocationToAtCFAPlusOffset(fp_reg_num, ptr_size * -2, true);
- row.SetRegisterLocationToAtCFAPlusOffset(pc_reg_num, ptr_size * -1, true);
+ row.GetCFAValue().SetIsRegisterPlusOffset(sp_reg_num, 8);
+ row.SetRegisterLocationToAtCFAPlusOffset(pc_reg_num, -8, false);
row.SetRegisterLocationToIsCFAPlusOffset(sp_reg_num, 0, true);
auto plan_sp = std::make_shared<UnwindPlan>(eRegisterKindDWARF);
diff --git a/lldb/test/API/lang/c/trampoline_stepping/Makefile b/lldb/test/API/lang/c/trampoline_stepping/Makefile
new file mode 100644
index 0000000000000..10495940055b6
--- /dev/null
+++ b/lldb/test/API/lang/c/trampoline_stepping/Makefile
@@ -0,0 +1,3 @@
+C_SOURCES := main.c
+
+include Makefile.rules
diff --git a/lldb/test/API/lang/c/trampoline_stepping/TestTrampolineStepping.py b/lldb/test/API/lang/c/trampoline_stepping/TestTrampolineStepping.py
new file mode 100644
index 0000000000000..bbf1dc8cb7fea
--- /dev/null
+++ b/lldb/test/API/lang/c/trampoline_stepping/TestTrampolineStepping.py
@@ -0,0 +1,100 @@
+"""Test that stepping in/out of trampolines works as expected."""
+
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class TestTrampoline(TestBase):
+ def setup(self, bkpt_str):
+ self.build()
+
+ _, _, thread, _ = lldbutil.run_to_source_breakpoint(
+ self, bkpt_str, lldb.SBFileSpec("main.c")
+ )
+ return thread
+
+ def test_direct_call(self):
+ thread = self.setup("Break here for direct")
+
+ # Sanity check that we start out in the correct function.
+ name = thread.frames[0].GetFunctionName()
+ self.assertIn("direct_trampoline_call", name)
+
+ # Check that stepping in will take us directly to the trampoline target.
+ thread.StepInto()
+ name = thread.frames[0].GetFunctionName()
+ self.assertIn("foo", name)
+
+ # Check that stepping out takes us back to the trampoline caller.
+ thread.StepOut()
+ name = thread.frames[0].GetFunctionName()
+ self.assertIn("direct_trampoline_call", name)
+
+ # Check that stepping over the end of the trampoline target
+ # takes us back to the trampoline caller.
+ thread.StepInto()
+ thread.StepOver()
+ name = thread.frames[0].GetFunctionName()
+ self.assertIn("direct_trampoline_call", name)
+
+ def test_chained_call(self):
+ thread = self.setup("Break here for chained")
+
+ # Sanity check that we start out in the correct function.
+ name = thread.frames[0].GetFunctionName()
+ self.assertIn("chained_trampoline_call", name)
+
+ # Check that stepping in will take us directly to the trampoline target.
+ thread.StepInto()
+ name = thread.frames[0].GetFunctionName()
+ self.assertIn("foo", name)
+
+ # Check that stepping out takes us back to the trampoline caller.
+ thread.StepOut()
+ name = thread.frames[0].GetFunctionName()
+ self.assertIn("chained_trampoline_call", name)
+
+ # Check that stepping over the end of the trampoline target
+ # takes us back to the trampoline caller.
+ thread.StepInto()
+ thread.StepOver()
+ name = thread.frames[0].GetFunctionName()
+ self.assertIn("chained_trampoline_call", name)
+
+ def test_trampoline_after_nodebug(self):
+ thread = self.setup("Break here for nodebug then trampoline")
+
+ # Sanity check that we start out in the correct function.
+ name = thread.frames[0].GetFunctionName()
+ self.assertIn("trampoline_after_nodebug", name)
+
+ # Check that stepping in will take us directly to the trampoline target.
+ thread.StepInto()
+ name = thread.frames[0].GetFunctionName()
+ self.assertIn("foo", name)
+
+ # Check that stepping out takes us back to the trampoline caller.
+ thread.StepOut()
+ name = thread.frames[0].GetFunctionName()
+ self.assertIn("trampoline_after_nodebug", name)
+
+ # Check that stepping over the end of the trampoline target
+ # takes us back to the trampoline caller.
+ thread.StepInto()
+ thread.StepOver()
+ name = thread.frames[0].GetFunctionName()
+ self.assertIn("trampoline_after_nodebug", name)
+
+ def test_unused_target(self):
+ thread = self.setup("Break here for unused")
+
+ # Sanity check that we start out in the correct function.
+ name = thread.frames[0].GetFunctionName()
+ self.assertIn("unused_target", name)
+
+ # Check that stepping into a trampoline that doesn't call its target
+ # jumps back to its caller.
+ thread.StepInto()
+ name = thread.frames[0].GetFunctionName()
+ self.assertIn("unused_target", name)
diff --git a/lldb/test/API/lang/c/trampoline_stepping/main.c b/lldb/test/API/lang/c/trampoline_stepping/main.c
new file mode 100644
index 0000000000000..cb98be00ca1f6
--- /dev/null
+++ b/lldb/test/API/lang/c/trampoline_stepping/main.c
@@ -0,0 +1,52 @@
+void foo(void) {}
+
+__attribute__((transparent_stepping))
+void bar(void) {
+ foo();
+}
+
+__attribute__((transparent_stepping))
+void baz(void) {
+ bar();
+}
+
+__attribute__((nodebug))
+void nodebug(void) {}
+
+__attribute__((transparent_stepping))
+void nodebug_then_trampoline(void) {
+ nodebug();
+ baz();
+}
+
+__attribute__((transparent_stepping))
+void doesnt_call_trampoline(void) {}
+
+void direct_trampoline_call(void) {
+ bar(); // Break here for direct
+ bar();
+}
+
+void chained_trampoline_call(void) {
+ baz(); // Break here for chained
+ baz();
+}
+
+void trampoline_after_nodebug(void) {
+ nodebug_then_trampoline(); // Break here for nodebug then trampoline
+ nodebug_then_trampoline();
+}
+
+void unused_target(void) {
+ doesnt_call_trampoline(); // Break here for unused
+}
+
+
+int main(void) {
+ direct_trampoline_call();
+ chained_trampoline_call();
+ trampoline_after_nodebug();
+ unused_target();
+ return 0;
+}
+
More information about the lldb-commits
mailing list