From 486a0320de16e69ec53ebe4ff96b512094f1ec5b Mon Sep 17 00:00:00 2001 From: Felipe de Azevedo Piovezan Date: Wed, 29 Jan 2025 10:50:07 -0800 Subject: [PATCH] [lldb][swift] Prefer assembly unwind plans for async registers The eh_frame section only describes registers that are callee saved; in other words, registers whose values will be restored before the function returns. Under this definition, the async register is not callee saved for the purposes of the swifttailcc ABI. Why? Because it's impossible to do so. When a funclet is about to tail call into something else, it will reuse the async register for the argument of the next funclet being tail called. However, the async register is guaranteed to be written to a specific stack slot as part of the frame formation; it is guaranteed to be written, *but not guaranteed to be restored*, as per the argument above. Because of this, we should not rely on a mechanism meant to describe registers that are restored (eh_frame). LLDB does not know about calling conventions, it just assumes they all follow the ARM calling convention as closely as possible. As such, when it tries to read information about the async register on eh_frame and finds no entry for that register, LLDB makes the assumption that "this register was not modified", which is correct for the "normal" ABI, but not for swifttailcc. The compiler did not add information for the async register in eh_frame because it is not a callee saved register. To address this, this commit makes a very targeted change in SwiftLanguageRuntime, preferring the assembly plans every time it attempts to recover the async register. This is a much smaller change than it looks: we're already using this same plan for the vast majority of cases, since compact unwinding is not valid at frame 0 and eh_frame is hardly ever produced in Apple platforms. --- .../Swift/SwiftLanguageRuntime.cpp | 17 +++- .../unwind/unwind_register_pressure/Makefile | 3 + .../TestSwiftAsyncUnwindRegisterPressure.py | 87 +++++++++++++++++++ .../unwind_register_pressure/main.swift | 66 ++++++++++++++ 4 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 lldb/test/API/lang/swift/async/unwind/unwind_register_pressure/Makefile create mode 100644 lldb/test/API/lang/swift/async/unwind/unwind_register_pressure/TestSwiftAsyncUnwindRegisterPressure.py create mode 100644 lldb/test/API/lang/swift/async/unwind/unwind_register_pressure/main.swift diff --git a/lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntime.cpp index fc976507b6f1e..947c4410fb11d 100644 --- a/lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntime.cpp +++ b/lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntime.cpp @@ -2206,6 +2206,21 @@ static llvm::Expected GetCFA(Process &process, RegisterContext ®ctx, cfa_loc.GetValueType()); } +static UnwindPlanSP GetUnwindPlanForAsyncRegister(FuncUnwinders &unwinders, + Target &target, + Thread &thread) { + // We cannot trust compiler emitted unwind plans, as they respect the + // swifttail calling convention, which assumes the async register is _not_ + // restored and therefore it is not tracked by compiler plans. If LLDB uses + // those plans, it may take "no info" to mean "register not clobbered". For + // those reasons, always favour the assembly plan first, it will try to track + // the async register by assuming the usual arm calling conventions. + if (UnwindPlanSP asm_plan = unwinders.GetAssemblyUnwindPlan(target, thread)) + return asm_plan; + // In the unlikely case the assembly plan is not available, try all others. + return unwinders.GetUnwindPlanAtNonCallSite(target, thread); +} + /// Attempts to use UnwindPlans that inspect assembly to recover the entry value /// of the async context register. This is a simplified version of the methods /// in RegisterContextUnwind, since plumbing access to those here would be @@ -2223,7 +2238,7 @@ static llvm::Expected ReadAsyncContextRegisterFromUnwind( Target &target = process.GetTarget(); UnwindPlanSP unwind_plan = - unwinders->GetUnwindPlanAtNonCallSite(target, regctx.GetThread()); + GetUnwindPlanForAsyncRegister(*unwinders, target, regctx.GetThread()); if (!unwind_plan) return llvm::createStringError( "SwiftLanguageRuntime: Failed to find non call site unwind plan at " diff --git a/lldb/test/API/lang/swift/async/unwind/unwind_register_pressure/Makefile b/lldb/test/API/lang/swift/async/unwind/unwind_register_pressure/Makefile new file mode 100644 index 0000000000000..adad7d16985c2 --- /dev/null +++ b/lldb/test/API/lang/swift/async/unwind/unwind_register_pressure/Makefile @@ -0,0 +1,3 @@ +SWIFT_SOURCES := main.swift +SWIFTFLAGS_EXTRAS := -parse-as-library -Xfrontend -Xllvm -Xfrontend --emit-dwarf-unwind=always +include Makefile.rules diff --git a/lldb/test/API/lang/swift/async/unwind/unwind_register_pressure/TestSwiftAsyncUnwindRegisterPressure.py b/lldb/test/API/lang/swift/async/unwind/unwind_register_pressure/TestSwiftAsyncUnwindRegisterPressure.py new file mode 100644 index 0000000000000..db9e5bbd9cada --- /dev/null +++ b/lldb/test/API/lang/swift/async/unwind/unwind_register_pressure/TestSwiftAsyncUnwindRegisterPressure.py @@ -0,0 +1,87 @@ +import lldb +from lldbsuite.test.decorators import * +import lldbsuite.test.lldbtest as lldbtest +import lldbsuite.test.lldbutil as lldbutil + + +# This test is a best effort attempt at creating a program with high register +# pressure while also making the linker avoid compact unwind. +class TestCase(lldbtest.TestBase): + + mydir = lldbtest.TestBase.compute_mydir(__file__) + + def set_breakpoints_all_instructions(self, target): + funclet_name = "$s1a12ASYNC___1___SiyYaFTY0_" + breakpoints = set() + + sym_ctx_list = target.FindFunctions(funclet_name) + self.assertEqual( + sym_ctx_list.GetSize(), + 1, + f"failed to get symbol context for {funclet_name}", + ) + function = sym_ctx_list[0].function + + instructions = list(function.GetInstructions(target)) + self.assertGreater(len(instructions), 0) + for instruction in instructions: + bp = target.BreakpointCreateBySBAddress(instruction.GetAddress()) + self.assertTrue( + bp.IsValid(), f"failed to set bp inside funclet {funclet_name}" + ) + breakpoints.add(bp.GetID()) + return breakpoints + + def check_unwind_ok(self, thread): + # Check that we see the virtual backtrace: + expected_funcnames = [ + "ASYNC___1___", + "ASYNC___2___", + "ASYNC___3___", + "ASYNC___4___", + "ASYNC___5___", + ] + frames = thread.frames + self.assertGreater( + len(frames), len(expected_funcnames), f"Invalid backtrace for {frames}" + ) + actual_funcnames = [ + frame.GetFunctionName() for frame in frames[: len(expected_funcnames)] + ] + for expected_name, actual_name in zip(expected_funcnames, actual_funcnames): + self.assertIn(expected_name, actual_name, f"Unexpected backtrace: {frames}") + + @swiftTest + @skipIf(oslist=["windows", "linux"]) + def test(self): + """Test that the debugger can unwind at all instructions of all funclets""" + self.build() + + source_file = lldb.SBFileSpec("main.swift") + target, process, _, bp = lldbutil.run_to_source_breakpoint( + self, "BREAK HERE", source_file + ) + target.DeleteAllBreakpoints() + + breakpoints = self.set_breakpoints_all_instructions(target) + num_breakpoints = len(breakpoints) + + # Reach most breakpoints and ensure we can unwind in that position. + while True: + process.Continue() + if process.GetState() == lldb.eStateExited: + break + thread = lldbutil.get_stopped_thread(process, lldb.eStopReasonBreakpoint) + self.assertTrue(thread.IsValid()) + bpid = thread.GetStopReasonDataAtIndex(0) + breakpoints.remove(bpid) + target.FindBreakpointByID(bpid).SetEnabled(False) + + self.check_unwind_ok(thread) + + # We will never hit all breakpoints we set, because of things like + # overflow handling or other unreachable traps. However, it's good to + # have some sanity check that we have hit at least a decent chunk of + # them. + breakpoints_not_hit = len(breakpoints) + self.assertLess(breakpoints_not_hit / num_breakpoints, 0.25) diff --git a/lldb/test/API/lang/swift/async/unwind/unwind_register_pressure/main.swift b/lldb/test/API/lang/swift/async/unwind/unwind_register_pressure/main.swift new file mode 100644 index 0000000000000..7eae31a22d4e4 --- /dev/null +++ b/lldb/test/API/lang/swift/async/unwind/unwind_register_pressure/main.swift @@ -0,0 +1,66 @@ +func work(_ objects: Any...) { + for object in objects { + print("Processing object of type: \(type(of: object))") + } +} + +func use(_ x: Int, _ y: Int) -> Int { + return x &* y &+ x &- y +} + +var arr: [Int] = [] + +func ASYNC___1___() async -> Int { + var a1 = 1, a2 = 2, a3 = 3, a4 = 4, a5 = 5 + print("BREAK HERE") + var a6 = 6, a7 = 7, a8 = 8, a9 = 9, a10 = 10 + var a11 = 11, a12 = 12, a13 = 13, a14 = 14, a15 = 15 + var a16 = 16, a17 = 17, a18 = 18, a19 = 19, a20 = 20 + var a21 = 21, a22 = 22, a23 = 23, a24 = 24, a25 = 25 + var a26 = 26, a27 = 27, a28 = 28, a29 = 29, a30 = 30 + a1 = use(a1, a2) + a3 = use(a3, a4) + a5 = use(a5, a6) + a7 = use(a7, a8) + a9 = use(a9, a10) + a11 = use(a11, a12) + a13 = use(a13, a14) + a15 = use(a15, a16) + a17 = use(a17, a18) + a19 = use(a19, a20) + a21 = use(a21, a22) + a23 = use(a23, a24) + a25 = use(a25, a26) + a27 = use(a27, a28) + a29 = use(a29, a30) + work(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24,a25, a26,a27, a28, a29, a30) + arr = [a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24,a25, a26,a27, a28, a29, a30] + return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10 + a11 + a12 + a13 + a14 + a15 + a16 + a17 + a18 + a19 +} + +func ASYNC___2___() async -> Int { + let result = await ASYNC___1___() + return result +} + +func ASYNC___3___() async -> Int { + let result = await ASYNC___2___() + return result +} + +func ASYNC___4___() async -> Int { + let result = await ASYNC___3___() + return result +} + +func ASYNC___5___() async -> Int { + let result = await ASYNC___4___() + return result +} + +@main struct Main { + static func main() async { + let result = await ASYNC___5___() + print(result) + } +}