Skip to content

Commit cfe1541

Browse files
Merge pull request #10186 from felipepiovezan/felipe/update_unwind_heuristic
[lldb] Change swift unwind heuristic for Q funclets
2 parents 08a4e18 + 384f280 commit cfe1541

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
@@ -2600,31 +2600,29 @@ static llvm::Expected<addr_t> ReadAsyncContextRegisterFromUnwind(
26002600
/// CFA of the currently executing function. This is the case at the start of
26012601
/// "Q" funclets, before the low level code changes the meaning of the async
26022602
/// register to not require the indirection.
2603-
/// This implementation detects the transition point by comparing the
2604-
/// continuation pointer in the async context with the currently executing
2605-
/// funclet given by the SymbolContext sc. If they are the same, the PC is
2606-
/// before the transition point.
2607-
/// FIXME: this fails in some recursive async functions. See: rdar://139676623
2603+
/// The end of the prologue approximates the transition point.
2604+
/// FIXME: In the few instructions between the end of the prologue and the
2605+
/// transition point, this approximation fails. rdar://139676623
26082606
static llvm::Expected<bool> IsIndirectContext(Process &process,
26092607
StringRef mangled_name,
2610-
addr_t async_reg,
2611-
SymbolContext &sc) {
2608+
Address pc, SymbolContext &sc) {
26122609
if (!SwiftLanguageRuntime::IsSwiftAsyncAwaitResumePartialFunctionSymbol(
26132610
mangled_name))
26142611
return false;
26152612

2616-
llvm::Expected<addr_t> continuation_ptr = ReadPtrFromAddr(
2617-
process, async_reg, /*offset*/ process.GetAddressByteSize());
2618-
if (!continuation_ptr)
2619-
return continuation_ptr.takeError();
2620-
2621-
if (sc.function)
2622-
return sc.function->GetAddressRange().ContainsLoadAddress(
2623-
*continuation_ptr, &process.GetTarget());
2624-
assert(sc.symbol);
2625-
Address continuation_addr;
2626-
continuation_addr.SetLoadAddress(*continuation_ptr, &process.GetTarget());
2627-
return sc.symbol->ContainsFileAddress(continuation_addr.GetFileAddress());
2613+
// This is checked prior to calling this function.
2614+
assert(sc.function || sc.symbol);
2615+
uint32_t prologue_size = sc.function ? sc.function->GetPrologueByteSize()
2616+
: sc.symbol->GetPrologueByteSize();
2617+
Address func_start_addr =
2618+
sc.function ? sc.function->GetAddressRange().GetBaseAddress()
2619+
: sc.symbol->GetAddress();
2620+
// Include one instruction after the prologue. This is where breakpoints
2621+
// by function name are set, so it's important to get this point right. This
2622+
// instruction is exactly at address "base + prologue", so adding 1
2623+
// in the range will do.
2624+
AddressRange prologue_range(func_start_addr, prologue_size + 1);
2625+
return prologue_range.ContainsLoadAddress(pc, &process.GetTarget());
26282626
}
26292627

26302628
// Examine the register state and detect the transition from a real
@@ -2694,7 +2692,7 @@ SwiftLanguageRuntime::GetRuntimeUnwindPlan(ProcessSP process_sp,
26942692
return log_expected(async_reg.takeError());
26952693

26962694
llvm::Expected<bool> maybe_indirect_context =
2697-
IsIndirectContext(*process_sp, mangled_name, *async_reg, sc);
2695+
IsIndirectContext(*process_sp, mangled_name, pc, sc);
26982696
if (!maybe_indirect_context)
26992697
return log_expected(maybe_indirect_context.takeError());
27002698

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)