Skip to content

Commit 486a032

Browse files
[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.
1 parent f379c52 commit 486a032

File tree

4 files changed

+172
-1
lines changed

4 files changed

+172
-1
lines changed

lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntime.cpp

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2206,6 +2206,21 @@ static llvm::Expected<addr_t> GetCFA(Process &process, RegisterContext &regctx,
22062206
cfa_loc.GetValueType());
22072207
}
22082208

2209+
static UnwindPlanSP GetUnwindPlanForAsyncRegister(FuncUnwinders &unwinders,
2210+
Target &target,
2211+
Thread &thread) {
2212+
// We cannot trust compiler emitted unwind plans, as they respect the
2213+
// swifttail calling convention, which assumes the async register is _not_
2214+
// restored and therefore it is not tracked by compiler plans. If LLDB uses
2215+
// those plans, it may take "no info" to mean "register not clobbered". For
2216+
// those reasons, always favour the assembly plan first, it will try to track
2217+
// the async register by assuming the usual arm calling conventions.
2218+
if (UnwindPlanSP asm_plan = unwinders.GetAssemblyUnwindPlan(target, thread))
2219+
return asm_plan;
2220+
// In the unlikely case the assembly plan is not available, try all others.
2221+
return unwinders.GetUnwindPlanAtNonCallSite(target, thread);
2222+
}
2223+
22092224
/// Attempts to use UnwindPlans that inspect assembly to recover the entry value
22102225
/// of the async context register. This is a simplified version of the methods
22112226
/// in RegisterContextUnwind, since plumbing access to those here would be
@@ -2223,7 +2238,7 @@ static llvm::Expected<addr_t> ReadAsyncContextRegisterFromUnwind(
22232238

22242239
Target &target = process.GetTarget();
22252240
UnwindPlanSP unwind_plan =
2226-
unwinders->GetUnwindPlanAtNonCallSite(target, regctx.GetThread());
2241+
GetUnwindPlanForAsyncRegister(*unwinders, target, regctx.GetThread());
22272242
if (!unwind_plan)
22282243
return llvm::createStringError(
22292244
"SwiftLanguageRuntime: Failed to find non call site unwind plan at "
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
SWIFT_SOURCES := main.swift
2+
SWIFTFLAGS_EXTRAS := -parse-as-library -Xfrontend -Xllvm -Xfrontend --emit-dwarf-unwind=always
3+
include Makefile.rules
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import lldb
2+
from lldbsuite.test.decorators import *
3+
import lldbsuite.test.lldbtest as lldbtest
4+
import lldbsuite.test.lldbutil as lldbutil
5+
6+
7+
# This test is a best effort attempt at creating a program with high register
8+
# pressure while also making the linker avoid compact unwind.
9+
class TestCase(lldbtest.TestBase):
10+
11+
mydir = lldbtest.TestBase.compute_mydir(__file__)
12+
13+
def set_breakpoints_all_instructions(self, target):
14+
funclet_name = "$s1a12ASYNC___1___SiyYaFTY0_"
15+
breakpoints = set()
16+
17+
sym_ctx_list = target.FindFunctions(funclet_name)
18+
self.assertEqual(
19+
sym_ctx_list.GetSize(),
20+
1,
21+
f"failed to get symbol context for {funclet_name}",
22+
)
23+
function = sym_ctx_list[0].function
24+
25+
instructions = list(function.GetInstructions(target))
26+
self.assertGreater(len(instructions), 0)
27+
for instruction in instructions:
28+
bp = target.BreakpointCreateBySBAddress(instruction.GetAddress())
29+
self.assertTrue(
30+
bp.IsValid(), f"failed to set bp inside funclet {funclet_name}"
31+
)
32+
breakpoints.add(bp.GetID())
33+
return breakpoints
34+
35+
def check_unwind_ok(self, thread):
36+
# Check that we see the virtual backtrace:
37+
expected_funcnames = [
38+
"ASYNC___1___",
39+
"ASYNC___2___",
40+
"ASYNC___3___",
41+
"ASYNC___4___",
42+
"ASYNC___5___",
43+
]
44+
frames = thread.frames
45+
self.assertGreater(
46+
len(frames), len(expected_funcnames), f"Invalid backtrace for {frames}"
47+
)
48+
actual_funcnames = [
49+
frame.GetFunctionName() for frame in frames[: len(expected_funcnames)]
50+
]
51+
for expected_name, actual_name in zip(expected_funcnames, actual_funcnames):
52+
self.assertIn(expected_name, actual_name, f"Unexpected backtrace: {frames}")
53+
54+
@swiftTest
55+
@skipIf(oslist=["windows", "linux"])
56+
def test(self):
57+
"""Test that the debugger can unwind at all instructions of all funclets"""
58+
self.build()
59+
60+
source_file = lldb.SBFileSpec("main.swift")
61+
target, process, _, bp = lldbutil.run_to_source_breakpoint(
62+
self, "BREAK HERE", source_file
63+
)
64+
target.DeleteAllBreakpoints()
65+
66+
breakpoints = self.set_breakpoints_all_instructions(target)
67+
num_breakpoints = len(breakpoints)
68+
69+
# Reach most breakpoints and ensure we can unwind in that position.
70+
while True:
71+
process.Continue()
72+
if process.GetState() == lldb.eStateExited:
73+
break
74+
thread = lldbutil.get_stopped_thread(process, lldb.eStopReasonBreakpoint)
75+
self.assertTrue(thread.IsValid())
76+
bpid = thread.GetStopReasonDataAtIndex(0)
77+
breakpoints.remove(bpid)
78+
target.FindBreakpointByID(bpid).SetEnabled(False)
79+
80+
self.check_unwind_ok(thread)
81+
82+
# We will never hit all breakpoints we set, because of things like
83+
# overflow handling or other unreachable traps. However, it's good to
84+
# have some sanity check that we have hit at least a decent chunk of
85+
# them.
86+
breakpoints_not_hit = len(breakpoints)
87+
self.assertLess(breakpoints_not_hit / num_breakpoints, 0.25)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
func work(_ objects: Any...) {
2+
for object in objects {
3+
print("Processing object of type: \(type(of: object))")
4+
}
5+
}
6+
7+
func use(_ x: Int, _ y: Int) -> Int {
8+
return x &* y &+ x &- y
9+
}
10+
11+
var arr: [Int] = []
12+
13+
func ASYNC___1___() async -> Int {
14+
var a1 = 1, a2 = 2, a3 = 3, a4 = 4, a5 = 5
15+
print("BREAK HERE")
16+
var a6 = 6, a7 = 7, a8 = 8, a9 = 9, a10 = 10
17+
var a11 = 11, a12 = 12, a13 = 13, a14 = 14, a15 = 15
18+
var a16 = 16, a17 = 17, a18 = 18, a19 = 19, a20 = 20
19+
var a21 = 21, a22 = 22, a23 = 23, a24 = 24, a25 = 25
20+
var a26 = 26, a27 = 27, a28 = 28, a29 = 29, a30 = 30
21+
a1 = use(a1, a2)
22+
a3 = use(a3, a4)
23+
a5 = use(a5, a6)
24+
a7 = use(a7, a8)
25+
a9 = use(a9, a10)
26+
a11 = use(a11, a12)
27+
a13 = use(a13, a14)
28+
a15 = use(a15, a16)
29+
a17 = use(a17, a18)
30+
a19 = use(a19, a20)
31+
a21 = use(a21, a22)
32+
a23 = use(a23, a24)
33+
a25 = use(a25, a26)
34+
a27 = use(a27, a28)
35+
a29 = use(a29, a30)
36+
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)
37+
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]
38+
return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10 + a11 + a12 + a13 + a14 + a15 + a16 + a17 + a18 + a19
39+
}
40+
41+
func ASYNC___2___() async -> Int {
42+
let result = await ASYNC___1___()
43+
return result
44+
}
45+
46+
func ASYNC___3___() async -> Int {
47+
let result = await ASYNC___2___()
48+
return result
49+
}
50+
51+
func ASYNC___4___() async -> Int {
52+
let result = await ASYNC___3___()
53+
return result
54+
}
55+
56+
func ASYNC___5___() async -> Int {
57+
let result = await ASYNC___4___()
58+
return result
59+
}
60+
61+
@main struct Main {
62+
static func main() async {
63+
let result = await ASYNC___5___()
64+
print(result)
65+
}
66+
}

0 commit comments

Comments
 (0)