Skip to content

Commit 384f280

Browse files
[lldb] Change swift unwind heuristic for Q funclets
Q funclets have an ambiguity region where the debugger doesn't know what the meaning of the x22 register is. Today, it works around this by sacrificing the ability to unwind recursive async functions. This patch changes the heuristic to, instead, sacrifice unwinding in a few instructions after the prologue, and before any branches. Such instructions should never be hit in the course of stepping or setting breakpoints, a user would need to go out of their way to stop in one of them. The new approach is: 1. If we are inside the prologue, or the first instruction after the prologue, assume x22 contains the indirect context. 2. Otherwise, assume it is the direct context. This approach fails for the few instructions shown below. This is what the assembly looks like for Q funclets in x86, with the first non-prologue instruction indicated by the arrow: ``` 0x100004140 <+0>: orq 0x3ee9(%rip), %rbp ; (void *)0x1000000000000000 0x100004147 <+7>: pushq %rbp 0x100004148 <+8>: pushq %r14 0x10000414a <+10>: leaq 0x8(%rsp), %rbp 0x10000414f <+15>: subq $0x38, %rsp -> 0x100004153 <+19>: movq (%r14), %rax 0x100004156 <+22>: movq %rax, -0x30(%rbp) << Fail zone start, inclusive 0x10000415a <+26>: movq %rbp, %rcx 0x10000415d <+29>: subq $0x8, %rcx 0x100004161 <+33>: movq %rax, (%rcx) << Fail zone end, inclusive 0x100004164 <+36>: movq 0x50(%rax), %rdi 0x100004168 <+40>: movq (%r14), %rcx 0x10000416b <+43>: movq %rbp, %rdx 0x10000416e <+46>: subq $0x8, %rdx 0x100004172 <+50>: movq %rcx, (%rdx) 0x100004175 <+53>: movq %rcx, 0x30(%rax) 0x100004179 <+57>: callq 0x100006036 ; symbol stub for: swift_task_dealloc ... ``` The same assembly for arm: ``` 0x100000c54 <+0>: orr x29, x29, #0x1000000000000000 0x100000c58 <+4>: sub sp, sp, #0x40 0x100000c5c <+8>: stp x29, x30, [sp, #0x30] 0x100000c60 <+12>: str x22, [sp, #0x28] 0x100000c64 <+16>: add x29, sp, #0x30 -> 0x100000c68 <+20>: ldr x9, [x22] 0x100000c6c <+24>: str x9, [sp] << Fail zone start, inclusive 0x100000c70 <+28>: mov x8, x29 0x100000c74 <+32>: sub x8, x8, #0x8 0x100000c78 <+36>: str x9, [x8] << Fail zone end, inclusive 0x100000c7c <+40>: ldr x0, [x9, #0x50] 0x100000c80 <+44>: ldr x8, [x22] 0x100000c84 <+48>: mov x10, x29 0x100000c88 <+52>: sub x10, x10, #0x8 0x100000c8c <+56>: str x8, [x10] 0x100000c90 <+60>: str x8, [x9, #0x30] 0x100000c94 <+64>: bl 0x100001cf4 ; symbol stub for: swift_task_dealloc ... ``` As a result, TestSwiftAsyncUnwindAllInstructions.py was changed to accept failure in those regions. A new test is added, showcasing a step operation that previously failed because of bad unwinding; it sets a breakpoint in a Q funclet and then step over. This is _not_ an artificially created situation, as a `step out` operation always takes the user to a Q funclet; being able to step-over from that point is crucial.
1 parent 745ab81 commit 384f280

File tree

5 files changed

+152
-20
lines changed

5 files changed

+152
-20
lines changed

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

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2559,31 +2559,29 @@ static llvm::Expected<addr_t> ReadAsyncContextRegisterFromUnwind(
25592559
/// CFA of the currently executing function. This is the case at the start of
25602560
/// "Q" funclets, before the low level code changes the meaning of the async
25612561
/// register to not require the indirection.
2562-
/// This implementation detects the transition point by comparing the
2563-
/// continuation pointer in the async context with the currently executing
2564-
/// funclet given by the SymbolContext sc. If they are the same, the PC is
2565-
/// before the transition point.
2566-
/// FIXME: this fails in some recursive async functions. See: rdar://139676623
2562+
/// The end of the prologue approximates the transition point.
2563+
/// FIXME: In the few instructions between the end of the prologue and the
2564+
/// transition point, this approximation fails. rdar://139676623
25672565
static llvm::Expected<bool> IsIndirectContext(Process &process,
25682566
StringRef mangled_name,
2569-
addr_t async_reg,
2570-
SymbolContext &sc) {
2567+
Address pc, SymbolContext &sc) {
25712568
if (!SwiftLanguageRuntime::IsSwiftAsyncAwaitResumePartialFunctionSymbol(
25722569
mangled_name))
25732570
return false;
25742571

2575-
llvm::Expected<addr_t> continuation_ptr = ReadPtrFromAddr(
2576-
process, async_reg, /*offset*/ process.GetAddressByteSize());
2577-
if (!continuation_ptr)
2578-
return continuation_ptr.takeError();
2579-
2580-
if (sc.function)
2581-
return sc.function->GetAddressRange().ContainsLoadAddress(
2582-
*continuation_ptr, &process.GetTarget());
2583-
assert(sc.symbol);
2584-
Address continuation_addr;
2585-
continuation_addr.SetLoadAddress(*continuation_ptr, &process.GetTarget());
2586-
return sc.symbol->ContainsFileAddress(continuation_addr.GetFileAddress());
2572+
// This is checked prior to calling this function.
2573+
assert(sc.function || sc.symbol);
2574+
uint32_t prologue_size = sc.function ? sc.function->GetPrologueByteSize()
2575+
: sc.symbol->GetPrologueByteSize();
2576+
Address func_start_addr =
2577+
sc.function ? sc.function->GetAddressRange().GetBaseAddress()
2578+
: sc.symbol->GetAddress();
2579+
// Include one instruction after the prologue. This is where breakpoints
2580+
// by function name are set, so it's important to get this point right. This
2581+
// instruction is exactly at address "base + prologue", so adding 1
2582+
// in the range will do.
2583+
AddressRange prologue_range(func_start_addr, prologue_size + 1);
2584+
return prologue_range.ContainsLoadAddress(pc, &process.GetTarget());
25872585
}
25882586

25892587
// Examine the register state and detect the transition from a real
@@ -2653,7 +2651,7 @@ SwiftLanguageRuntime::GetRuntimeUnwindPlan(ProcessSP process_sp,
26532651
return log_expected(async_reg.takeError());
26542652

26552653
llvm::Expected<bool> maybe_indirect_context =
2656-
IsIndirectContext(*process_sp, mangled_name, *async_reg, sc);
2654+
IsIndirectContext(*process_sp, mangled_name, pc, sc);
26572655
if (!maybe_indirect_context)
26582656
return log_expected(maybe_indirect_context.takeError());
26592657

lldb/test/API/lang/swift/async/unwind/unwind_in_all_instructions/TestSwiftAsyncUnwindAllInstructions.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,80 @@ def set_breakpoints_all_funclets(self, target):
6262
breakpoints.add(bp.GetID())
6363
return breakpoints
6464

65+
unwind_fail_range_cache = dict()
66+
67+
# There are challenges when unwinding Q funclets ("await resume"): LLDB cannot
68+
# detect the transition point where x22 stops containing the indirect context,
69+
# and instead contains the direct context.
70+
# Up to and including the first non-prologue instruction, LLDB correctly assumes
71+
# it is the indirect context.
72+
# After that, it assume x22 contains the direct context. There are a few
73+
# instructions where this is not true; this function computes a range that
74+
# includes such instructions, so the test may skip checks while stopped in them.
75+
def compute_unwind_fail_range(self, function, target):
76+
name = function.GetName()
77+
if name in TestCase.unwind_fail_range_cache:
78+
return TestCase.unwind_fail_range_cache[name]
79+
80+
if "await resume" not in function.GetName():
81+
TestCase.unwind_fail_range_cache[name] = range(0)
82+
return range(0)
83+
84+
first_pc_after_prologue = function.GetStartAddress()
85+
first_pc_after_prologue.OffsetAddress(function.GetPrologueByteSize())
86+
first_bad_instr = None
87+
first_good_instr = None
88+
for instr in function.GetInstructions(target):
89+
instr_addr = instr.GetAddress()
90+
91+
# The first bad instruction is approximately the second instruction after the prologue
92+
# In actuality, it is at some point after that.
93+
if first_bad_instr is None and (
94+
instr_addr.GetFileAddress() > first_pc_after_prologue.GetFileAddress()
95+
):
96+
first_bad_instr = instr
97+
continue
98+
99+
# The first good instr is approximately the branch to swift_task_dealloc.
100+
# In actuality, it is at some point before that.
101+
if "swift_task_dealloc" in instr.GetComment(target):
102+
first_good_instr = instr
103+
break
104+
105+
# If inside the bad range, no branches can be found.
106+
# If this happens, this test must fail so we know unwinding will be broken during stepping.
107+
if first_bad_instr is not None:
108+
# GetControlFlowKind is only implemented for x86.
109+
if "x86" in target.GetTriple():
110+
self.assertEqual(
111+
instr.GetControlFlowKind(target),
112+
lldb.eInstructionControlFlowKindOther,
113+
str(instr),
114+
)
115+
116+
self.assertNotEqual(first_bad_instr, None)
117+
self.assertNotEqual(first_good_instr, None)
118+
119+
fail_range = range(
120+
first_bad_instr.GetAddress().GetFileAddress(),
121+
first_good_instr.GetAddress().GetFileAddress(),
122+
)
123+
TestCase.unwind_fail_range_cache[name] = fail_range
124+
return fail_range
125+
126+
def should_skip_Q_funclet(self, thread):
127+
current_frame = thread.frames[0]
128+
function = current_frame.GetFunction()
129+
fail_range = self.compute_unwind_fail_range(
130+
function, thread.GetProcess().GetTarget()
131+
)
132+
133+
current_pc = current_frame.GetPCAddress()
134+
return current_pc.GetFileAddress() in fail_range
135+
65136
def check_unwind_ok(self, thread, bpid):
137+
if self.should_skip_Q_funclet(thread):
138+
return
66139
# Check that we see the virtual backtrace:
67140
expected_funcnames = [
68141
"ASYNC___1___",
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
3+
include Makefile.rules
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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+
class TestCase(lldbtest.TestBase):
8+
9+
mydir = lldbtest.TestBase.compute_mydir(__file__)
10+
11+
unwind_fail_range_cache = dict()
12+
13+
@swiftTest
14+
@skipIf(oslist=["windows", "linux"])
15+
def test(self):
16+
"""Test that the debugger can unwind at all instructions of all funclets"""
17+
self.build()
18+
19+
source_file = lldb.SBFileSpec("main.swift")
20+
target, process, thread, bkpt = lldbutil.run_to_name_breakpoint(
21+
self, "$s1a9factorialyS2iYaFTQ1_"
22+
)
23+
24+
# Ensure we are on the last factorial call which recurses (n == 1).
25+
frame = thread.frames[0]
26+
result = frame.EvaluateExpression("n == 1")
27+
self.assertSuccess(result.GetError())
28+
self.assertEqual(result.GetSummary(), "true")
29+
30+
# Disable the breakpoint and step over the call.
31+
bkpt.SetEnabled(False)
32+
33+
# Make sure we are still in the frame of n == 1.
34+
thread.StepOver()
35+
frame = thread.frames[0]
36+
result = frame.EvaluateExpression("n == 1")
37+
self.assertSuccess(result.GetError())
38+
self.assertEqual(result.GetSummary(), "true")
39+
40+
thread.StepOver()
41+
frame = thread.frames[0]
42+
result = frame.EvaluateExpression("n == 1")
43+
self.assertSuccess(result.GetError())
44+
self.assertEqual(result.GetSummary(), "true")
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
func factorial(_ n: Int) async -> Int {
2+
if (n == 0) {
3+
return 1;
4+
}
5+
let n1 = await factorial(n - 1)
6+
return n * n1
7+
}
8+
9+
@main struct Main {
10+
static func main() async {
11+
let result = await factorial(10)
12+
print(result)
13+
}
14+
}

0 commit comments

Comments
 (0)