-
Notifications
You must be signed in to change notification settings - Fork 344
[lldb] Use consistent CFA before/after prologue of async functions #4806
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2462,33 +2462,49 @@ SwiftLanguageRuntime::GetRuntimeUnwindPlan(ProcessSP process_sp, | |
eSymbolContextSymbol)) | ||
return UnwindPlanSP(); | ||
|
||
Address func_start_addr; | ||
uint32_t prologue_size; | ||
ConstString mangled_name; | ||
if (sc.function) { | ||
Address func_start_addr = sc.function->GetAddressRange().GetBaseAddress(); | ||
AddressRange prologue_range(func_start_addr, | ||
sc.function->GetPrologueByteSize()); | ||
if (prologue_range.ContainsLoadAddress(pc, &target) || | ||
func_start_addr == pc) { | ||
return UnwindPlanSP(); | ||
} | ||
func_start_addr = sc.function->GetAddressRange().GetBaseAddress(); | ||
prologue_size = sc.function->GetPrologueByteSize(); | ||
mangled_name = sc.function->GetMangled().GetMangledName(); | ||
} else if (sc.symbol) { | ||
Address func_start_addr = sc.symbol->GetAddress(); | ||
AddressRange prologue_range(func_start_addr, | ||
sc.symbol->GetPrologueByteSize()); | ||
if (prologue_range.ContainsLoadAddress(pc, &target) || | ||
func_start_addr == pc) { | ||
return UnwindPlanSP(); | ||
} | ||
func_start_addr = sc.symbol->GetAddress(); | ||
prologue_size = sc.symbol->GetPrologueByteSize(); | ||
mangled_name = sc.symbol->GetMangled().GetMangledName(); | ||
} else { | ||
return UnwindPlanSP(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We often just write There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer that too, I was following suit of the rest of the function. How about I change these all when I do a NFC refactor and documentation? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Personal preference, I prefer showing the explicitly returned object type but I know that's not the style in SwiftLanguageRuntime, feel free to change these to be a better fit. |
||
} | ||
|
||
addr_t saved_fp = LLDB_INVALID_ADDRESS; | ||
Status error; | ||
if (!process_sp->ReadMemory(fp, &saved_fp, 8, error)) | ||
return UnwindPlanSP(); | ||
AddressRange prologue_range(func_start_addr, prologue_size); | ||
bool in_prologue = (func_start_addr == pc || | ||
prologue_range.ContainsLoadAddress(pc, &target)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just curious about this - is it a performance concern, comparing the pc to function start addr, trying to avoid the method call? It seems unnecessary. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no intentions on my part, this is a refactor of the existing code which had both the equality check and the Contains check. Good point, both aren't needed. |
||
|
||
// Get the high nibble of the dreferenced fp; if the 60th bit is set, | ||
// this is the transition to a swift async AsyncContext chain. | ||
if ((saved_fp & (0xfULL << 60)) >> 60 != 1) | ||
return UnwindPlanSP(); | ||
if (in_prologue) { | ||
if (!IsAnySwiftAsyncFunctionSymbol(mangled_name.GetStringRef())) | ||
return UnwindPlanSP(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are any of these error paths log-worthy and would point to future ABI changes, or do they just mean that this isn't an async function? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call, I will add logs! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's the possibility of a proper async function but not using swift mangling. I noticed some like that. Perhaps people could hand roll such things. |
||
} else { | ||
addr_t saved_fp = LLDB_INVALID_ADDRESS; | ||
Status error; | ||
if (!process_sp->ReadMemory(fp, &saved_fp, 8, error)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That hardcoded "8" makes me nervous :-) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. This is moved code, not new, but I can probably do something about it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. swift async is only supported on 64-bit targets. At the beginning we call |
||
return UnwindPlanSP(); | ||
|
||
// Get the high nibble of the dreferenced fp; if the 60th bit is set, | ||
// this is the transition to a swift async AsyncContext chain. | ||
if ((saved_fp & (0xfULL << 60)) >> 60 != 1) | ||
return UnwindPlanSP(); | ||
} | ||
|
||
// The coroutine funclets split from an async function have 2 different ABIs: | ||
// - Async suspend partial functions and the first funclet get their async | ||
// context directly in the async register. | ||
// - Async await resume partial functions take their context indirectly, it | ||
// needs to be dereferenced to get the actual function's context. | ||
// The debug info for locals reflects this difference, so our unwinding of the | ||
// context register needs to reflect it too. | ||
bool indirect_context = | ||
IsSwiftAsyncAwaitResumePartialFunctionSymbol(mangled_name.GetStringRef()); | ||
|
||
UnwindPlan::RowSP row(new UnwindPlan::Row); | ||
const int32_t ptr_size = 8; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't we get this from Target? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should, this is also pre-existing :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's fine to parameterize these, but swift async is only supported on 64-bit targets. we could have a |
||
|
@@ -2529,30 +2545,30 @@ SwiftLanguageRuntime::GetRuntimeUnwindPlan(ProcessSP process_sp, | |
else | ||
llvm_unreachable("Unsupported architecture"); | ||
|
||
row->GetCFAValue().SetIsDWARFExpression(expr, expr_size); | ||
// The coroutine funclets split from an async function have 2 different ABIs: | ||
// - Async suspend partial functions and the first funclet get their async | ||
// context directly in the async register. | ||
// - Async await resume partial functions take their context indirectly, it | ||
// needs to be dereferenced to get the actual function's context. | ||
// The debug info for locals reflects this difference, so our unwinding of the | ||
// context register needs to reflect it too. | ||
bool indirect_context = | ||
sc.symbol ? IsSwiftAsyncAwaitResumePartialFunctionSymbol( | ||
sc.symbol->GetMangled().GetMangledName().GetStringRef()) | ||
: false; | ||
if (in_prologue) { | ||
if (indirect_context) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this condition only relevant in the prologue? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the prologue the way we set up the unwind to find the async context chain is different than mid-function when it's been set up. I expect the real difference is "am I on the first instruction" versus "am I in the middle of the function", but because you can interrupt programs asynchronously, you could be on any instruction. I think that's right. Dave? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @adrian-prantl |
||
row->GetCFAValue().SetIsRegisterDereferenced(regnums->async_ctx_regnum); | ||
else | ||
row->GetCFAValue().SetIsRegisterPlusOffset(regnums->async_ctx_regnum, 0); | ||
} else { | ||
row->GetCFAValue().SetIsDWARFExpression(expr, expr_size); | ||
} | ||
|
||
if (indirect_context) { | ||
// In a "resume" coroutine, the passed context argument needs to be | ||
// dereferenced once to get the context. This is reflected in the debug | ||
// info so we need to account for it and report am async register value | ||
// that needs to be dereferenced to get to the context. | ||
// Note that the size passed for the DWARF expression is the size of the | ||
// array minus one. This skips the last deref for this use. | ||
assert(expr[expr_size - 1] == llvm::dwarf::DW_OP_deref && | ||
"Should skip a deref"); | ||
row->SetRegisterLocationToIsDWARFExpression(regnums->async_ctx_regnum, expr, | ||
expr_size - 1, false); | ||
if (in_prologue) { | ||
row->SetRegisterLocationToSame(regnums->async_ctx_regnum, false); | ||
} else { | ||
Comment on lines
2557
to
+2560
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @adrian-prantl here's some other logic conditional on |
||
// In a "resume" coroutine, the passed context argument needs to be | ||
// dereferenced once to get the context. This is reflected in the debug | ||
// info so we need to account for it and report am async register value | ||
// that needs to be dereferenced to get to the context. | ||
// Note that the size passed for the DWARF expression is the size of the | ||
// array minus one. This skips the last deref for this use. | ||
assert(expr[expr_size - 1] == llvm::dwarf::DW_OP_deref && | ||
"Should skip a deref"); | ||
row->SetRegisterLocationToIsDWARFExpression(regnums->async_ctx_regnum, | ||
expr, expr_size - 1, false); | ||
} | ||
} else { | ||
// In the first part of a split async function, the context is passed | ||
// directly, so we can use the CFA value directly. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,55 +9,38 @@ class TestCase(lldbtest.TestBase): | |
|
||
@swiftTest | ||
@skipIf(oslist=['windows', 'linux']) | ||
@expectedFailureAll(bugnumber="rdar://88142757") | ||
def test(self): | ||
"""Test `frame variable` in async functions""" | ||
self.build() | ||
|
||
# Setting a breakpoint on "inner" results in a breakpoint at the start | ||
# of each coroutine "funclet" function. | ||
_, process, _, _ = lldbutil.run_to_name_breakpoint(self, 'inner') | ||
|
||
# The step-over actions in the below commands may not be needed in the | ||
# future, but for now they are. This comment describes why. Take the | ||
# following line of code: | ||
# let x = await asyncFunc() | ||
# Some breakpoints, including the ones in this test, resolve to | ||
# locations that are at the start of resume functions. At the start of | ||
# the resume function, the assignment may not be complete. In order to | ||
# ensure assignment takes place, step-over is used to take execution to | ||
# the next line. | ||
|
||
stop_num = 0 | ||
while process.state == lldb.eStateStopped: | ||
thread = process.GetSelectedThread() | ||
frame = thread.frames[0] | ||
if stop_num == 0: | ||
# Start of the function. | ||
pass | ||
elif stop_num == 1: | ||
# After first await, read `a`. | ||
a = frame.FindVariable("a") | ||
self.assertTrue(a.IsValid()) | ||
self.assertEqual(a.unsigned, 0) | ||
# Step to complete `a`'s assignment (stored in the stack). | ||
thread.StepOver() | ||
self.assertGreater(a.unsigned, 0) | ||
elif stop_num == 2: | ||
# After second, read `a` and `b`. | ||
# At this point, `a` can be read from the async context. | ||
a = frame.FindVariable("a") | ||
self.assertTrue(a.IsValid()) | ||
self.assertGreater(a.unsigned, 0) | ||
b = frame.FindVariable("b") | ||
self.assertTrue(b.IsValid()) | ||
self.assertEqual(b.unsigned, 0) | ||
# Step to complete `b`'s assignment (stored in the stack). | ||
thread.StepOver() | ||
self.assertGreater(b.unsigned, 0) | ||
else: | ||
# Unexpected stop. | ||
self.assertTrue(False) | ||
|
||
stop_num += 1 | ||
process.Continue() | ||
source_file = lldb.SBFileSpec("main.swift") | ||
target, process, _, _ = lldbutil.run_to_source_breakpoint( | ||
self, "// break one", source_file) | ||
|
||
# At "break one", only the `a` variable should have a value. | ||
frame = process.GetSelectedThread().frames[0] | ||
a = frame.FindVariable("a") | ||
self.assertTrue(a.IsValid()) | ||
self.assertGreater(a.unsigned, 0) | ||
b = frame.FindVariable("b") | ||
self.assertTrue(b.IsValid()) | ||
self.assertEqual(b.unsigned, 0) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure if this is actually better, but FYI: swift-lldb has There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TIL |
||
|
||
# The first breakpoint resolves to multiple locations, but only the | ||
# first location is needed. Now that we've stopped, delete it to | ||
# prevent the other locations from interrupting the test. | ||
target.DeleteAllBreakpoints() | ||
|
||
# Setup, and run to, the next breakpoint. | ||
target.BreakpointCreateBySourceRegex("// break two", source_file) | ||
self.setAsync(False) | ||
process.Continue() | ||
|
||
# At "break two", both `a` and `b` should have values. | ||
frame = process.GetSelectedThread().frames[0] | ||
a = frame.FindVariable("a") | ||
self.assertTrue(a.IsValid()) | ||
self.assertGreater(a.unsigned, 0) | ||
b = frame.FindVariable("b") | ||
self.assertTrue(b.IsValid()) | ||
self.assertGreater(b.unsigned, 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are sc.function and sc.symbol the same type? I wonder if this could be
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They aren't the same type, I did have the same thought. Maybe we can extract out a shared interface that we can use in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For these limit set of calls, Symbol and Function behave similarly, but Function can provide many additional things and Symbol is pretty much just this. (they also have very different ways of determining the prologue byte size).