Skip to content

Commit be3823b

Browse files
authored
[lldb] Use consistent CFA before/after prologue of async functions (#4806)
Previously, `SwiftLanguageRuntime::GetRuntimeUnwindPlan` would not generate an _async_ unwind plan when stopped inside the prologue. The reason was that the logic couldn't distinguish between an async function and a sync function called by an async function. This happens because – in a prologue, the register values may make it look like an async function (specifically the extended frame marker bit set on the frame pointer). To determine whether lldb is stopped in an async function, in addition to checking the extended frame marker, it can look for marker nodes in the symbol's demangle tree. This all seemed fine initially, but then we discovered some logic bugs within thread plans. The logic bugs were caused by CFA values varying at different parts of the function. By not returning an async unwind plan during the prologue, the effect is that the function call gets a standard (thread based) CFA (Canonical Frame Address). The standard CFA is the stack pointer ($sp) value at the call site. However once execution proceeds past the prologue, for the same function, lldb returns an async unwind plan. For an async unwind, the CFA is taken to be the async context passed into the function (`x22` on arm64, `r14` on x86-64). The problem is that now the CFA varies across the function. From the DWARF standard: > The algorithm to compute CFA changes as you progress through the prologue and epilogue code. (By definition, the CFA value does not change.) Between the logic bugs and DWARF, it's best to keep the CFA consistent throughout a function. This change does that by returning an async unwind plan even in the prologue. This makes the unwind plan logic more branch-y and complex than it was. A follow up change is to refactor this code as well as document it better. For diff readability, those changes will come separately. rdar://88142757
1 parent fd1ac0e commit be3823b

File tree

3 files changed

+93
-94
lines changed

3 files changed

+93
-94
lines changed

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

Lines changed: 60 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2462,33 +2462,49 @@ SwiftLanguageRuntime::GetRuntimeUnwindPlan(ProcessSP process_sp,
24622462
eSymbolContextSymbol))
24632463
return UnwindPlanSP();
24642464

2465+
Address func_start_addr;
2466+
uint32_t prologue_size;
2467+
ConstString mangled_name;
24652468
if (sc.function) {
2466-
Address func_start_addr = sc.function->GetAddressRange().GetBaseAddress();
2467-
AddressRange prologue_range(func_start_addr,
2468-
sc.function->GetPrologueByteSize());
2469-
if (prologue_range.ContainsLoadAddress(pc, &target) ||
2470-
func_start_addr == pc) {
2471-
return UnwindPlanSP();
2472-
}
2469+
func_start_addr = sc.function->GetAddressRange().GetBaseAddress();
2470+
prologue_size = sc.function->GetPrologueByteSize();
2471+
mangled_name = sc.function->GetMangled().GetMangledName();
24732472
} else if (sc.symbol) {
2474-
Address func_start_addr = sc.symbol->GetAddress();
2475-
AddressRange prologue_range(func_start_addr,
2476-
sc.symbol->GetPrologueByteSize());
2477-
if (prologue_range.ContainsLoadAddress(pc, &target) ||
2478-
func_start_addr == pc) {
2479-
return UnwindPlanSP();
2480-
}
2473+
func_start_addr = sc.symbol->GetAddress();
2474+
prologue_size = sc.symbol->GetPrologueByteSize();
2475+
mangled_name = sc.symbol->GetMangled().GetMangledName();
2476+
} else {
2477+
return UnwindPlanSP();
24812478
}
24822479

2483-
addr_t saved_fp = LLDB_INVALID_ADDRESS;
2484-
Status error;
2485-
if (!process_sp->ReadMemory(fp, &saved_fp, 8, error))
2486-
return UnwindPlanSP();
2480+
AddressRange prologue_range(func_start_addr, prologue_size);
2481+
bool in_prologue = (func_start_addr == pc ||
2482+
prologue_range.ContainsLoadAddress(pc, &target));
24872483

2488-
// Get the high nibble of the dreferenced fp; if the 60th bit is set,
2489-
// this is the transition to a swift async AsyncContext chain.
2490-
if ((saved_fp & (0xfULL << 60)) >> 60 != 1)
2491-
return UnwindPlanSP();
2484+
if (in_prologue) {
2485+
if (!IsAnySwiftAsyncFunctionSymbol(mangled_name.GetStringRef()))
2486+
return UnwindPlanSP();
2487+
} else {
2488+
addr_t saved_fp = LLDB_INVALID_ADDRESS;
2489+
Status error;
2490+
if (!process_sp->ReadMemory(fp, &saved_fp, 8, error))
2491+
return UnwindPlanSP();
2492+
2493+
// Get the high nibble of the dreferenced fp; if the 60th bit is set,
2494+
// this is the transition to a swift async AsyncContext chain.
2495+
if ((saved_fp & (0xfULL << 60)) >> 60 != 1)
2496+
return UnwindPlanSP();
2497+
}
2498+
2499+
// The coroutine funclets split from an async function have 2 different ABIs:
2500+
// - Async suspend partial functions and the first funclet get their async
2501+
// context directly in the async register.
2502+
// - Async await resume partial functions take their context indirectly, it
2503+
// needs to be dereferenced to get the actual function's context.
2504+
// The debug info for locals reflects this difference, so our unwinding of the
2505+
// context register needs to reflect it too.
2506+
bool indirect_context =
2507+
IsSwiftAsyncAwaitResumePartialFunctionSymbol(mangled_name.GetStringRef());
24922508

24932509
UnwindPlan::RowSP row(new UnwindPlan::Row);
24942510
const int32_t ptr_size = 8;
@@ -2529,30 +2545,30 @@ SwiftLanguageRuntime::GetRuntimeUnwindPlan(ProcessSP process_sp,
25292545
else
25302546
llvm_unreachable("Unsupported architecture");
25312547

2532-
row->GetCFAValue().SetIsDWARFExpression(expr, expr_size);
2533-
// The coroutine funclets split from an async function have 2 different ABIs:
2534-
// - Async suspend partial functions and the first funclet get their async
2535-
// context directly in the async register.
2536-
// - Async await resume partial functions take their context indirectly, it
2537-
// needs to be dereferenced to get the actual function's context.
2538-
// The debug info for locals reflects this difference, so our unwinding of the
2539-
// context register needs to reflect it too.
2540-
bool indirect_context =
2541-
sc.symbol ? IsSwiftAsyncAwaitResumePartialFunctionSymbol(
2542-
sc.symbol->GetMangled().GetMangledName().GetStringRef())
2543-
: false;
2548+
if (in_prologue) {
2549+
if (indirect_context)
2550+
row->GetCFAValue().SetIsRegisterDereferenced(regnums->async_ctx_regnum);
2551+
else
2552+
row->GetCFAValue().SetIsRegisterPlusOffset(regnums->async_ctx_regnum, 0);
2553+
} else {
2554+
row->GetCFAValue().SetIsDWARFExpression(expr, expr_size);
2555+
}
25442556

25452557
if (indirect_context) {
2546-
// In a "resume" coroutine, the passed context argument needs to be
2547-
// dereferenced once to get the context. This is reflected in the debug
2548-
// info so we need to account for it and report am async register value
2549-
// that needs to be dereferenced to get to the context.
2550-
// Note that the size passed for the DWARF expression is the size of the
2551-
// array minus one. This skips the last deref for this use.
2552-
assert(expr[expr_size - 1] == llvm::dwarf::DW_OP_deref &&
2553-
"Should skip a deref");
2554-
row->SetRegisterLocationToIsDWARFExpression(regnums->async_ctx_regnum, expr,
2555-
expr_size - 1, false);
2558+
if (in_prologue) {
2559+
row->SetRegisterLocationToSame(regnums->async_ctx_regnum, false);
2560+
} else {
2561+
// In a "resume" coroutine, the passed context argument needs to be
2562+
// dereferenced once to get the context. This is reflected in the debug
2563+
// info so we need to account for it and report am async register value
2564+
// that needs to be dereferenced to get to the context.
2565+
// Note that the size passed for the DWARF expression is the size of the
2566+
// array minus one. This skips the last deref for this use.
2567+
assert(expr[expr_size - 1] == llvm::dwarf::DW_OP_deref &&
2568+
"Should skip a deref");
2569+
row->SetRegisterLocationToIsDWARFExpression(regnums->async_ctx_regnum,
2570+
expr, expr_size - 1, false);
2571+
}
25562572
} else {
25572573
// In the first part of a split async function, the context is passed
25582574
// directly, so we can use the CFA value directly.

lldb/test/API/lang/swift/async/frame/variable/TestSwiftAsyncFrameVar.py

Lines changed: 31 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -9,55 +9,38 @@ class TestCase(lldbtest.TestBase):
99

1010
@swiftTest
1111
@skipIf(oslist=['windows', 'linux'])
12-
@expectedFailureAll(bugnumber="rdar://88142757")
1312
def test(self):
1413
"""Test `frame variable` in async functions"""
1514
self.build()
1615

17-
# Setting a breakpoint on "inner" results in a breakpoint at the start
18-
# of each coroutine "funclet" function.
19-
_, process, _, _ = lldbutil.run_to_name_breakpoint(self, 'inner')
20-
21-
# The step-over actions in the below commands may not be needed in the
22-
# future, but for now they are. This comment describes why. Take the
23-
# following line of code:
24-
# let x = await asyncFunc()
25-
# Some breakpoints, including the ones in this test, resolve to
26-
# locations that are at the start of resume functions. At the start of
27-
# the resume function, the assignment may not be complete. In order to
28-
# ensure assignment takes place, step-over is used to take execution to
29-
# the next line.
30-
31-
stop_num = 0
32-
while process.state == lldb.eStateStopped:
33-
thread = process.GetSelectedThread()
34-
frame = thread.frames[0]
35-
if stop_num == 0:
36-
# Start of the function.
37-
pass
38-
elif stop_num == 1:
39-
# After first await, read `a`.
40-
a = frame.FindVariable("a")
41-
self.assertTrue(a.IsValid())
42-
self.assertEqual(a.unsigned, 0)
43-
# Step to complete `a`'s assignment (stored in the stack).
44-
thread.StepOver()
45-
self.assertGreater(a.unsigned, 0)
46-
elif stop_num == 2:
47-
# After second, read `a` and `b`.
48-
# At this point, `a` can be read from the async context.
49-
a = frame.FindVariable("a")
50-
self.assertTrue(a.IsValid())
51-
self.assertGreater(a.unsigned, 0)
52-
b = frame.FindVariable("b")
53-
self.assertTrue(b.IsValid())
54-
self.assertEqual(b.unsigned, 0)
55-
# Step to complete `b`'s assignment (stored in the stack).
56-
thread.StepOver()
57-
self.assertGreater(b.unsigned, 0)
58-
else:
59-
# Unexpected stop.
60-
self.assertTrue(False)
61-
62-
stop_num += 1
63-
process.Continue()
16+
source_file = lldb.SBFileSpec("main.swift")
17+
target, process, _, _ = lldbutil.run_to_source_breakpoint(
18+
self, "// break one", source_file)
19+
20+
# At "break one", only the `a` variable should have a value.
21+
frame = process.GetSelectedThread().frames[0]
22+
a = frame.FindVariable("a")
23+
self.assertTrue(a.IsValid())
24+
self.assertGreater(a.unsigned, 0)
25+
b = frame.FindVariable("b")
26+
self.assertTrue(b.IsValid())
27+
self.assertEqual(b.unsigned, 0)
28+
29+
# The first breakpoint resolves to multiple locations, but only the
30+
# first location is needed. Now that we've stopped, delete it to
31+
# prevent the other locations from interrupting the test.
32+
target.DeleteAllBreakpoints()
33+
34+
# Setup, and run to, the next breakpoint.
35+
target.BreakpointCreateBySourceRegex("// break two", source_file)
36+
self.setAsync(False)
37+
process.Continue()
38+
39+
# At "break two", both `a` and `b` should have values.
40+
frame = process.GetSelectedThread().frames[0]
41+
a = frame.FindVariable("a")
42+
self.assertTrue(a.IsValid())
43+
self.assertGreater(a.unsigned, 0)
44+
b = frame.FindVariable("b")
45+
self.assertTrue(b.IsValid())
46+
self.assertGreater(b.unsigned, 0)

lldb/test/API/lang/swift/async/frame/variable/main.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ func randInt(_ i: Int) async -> Int {
44

55
func inner() async {
66
let a = await randInt(30)
7-
let b = await randInt(a + 11)
8-
use(a, b)
7+
let b = await randInt(a + 11) // break one
8+
use(a, b) // break two
99
}
1010

1111
func use<T>(_ t: T...) {}

0 commit comments

Comments
 (0)