Skip to content

[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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 60 additions & 44 deletions lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();

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

if (sc.function)
  func_info = sc.function;
else if (sc.symbol)
  func_info = sc.symbol;
Address func_start_addr = func_info->GetAddress();
...

Copy link
Author

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.

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).

} else {
return UnwindPlanSP();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We often just write return {};

Copy link
Author

Choose a reason for hiding this comment

The 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?

Choose a reason for hiding this comment

The 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));

Choose a reason for hiding this comment

The 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.

Copy link
Author

@kastiglione kastiglione Jun 7, 2022

Choose a reason for hiding this comment

The 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();

Choose a reason for hiding this comment

The 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?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, I will add logs!

Copy link
Author

Choose a reason for hiding this comment

The 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))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That hardcoded "8" makes me nervous :-)

Copy link
Author

Choose a reason for hiding this comment

The 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.

Choose a reason for hiding this comment

The 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 GetAsyncUnwindRegisterNumbers() which only returns regnums for x86_64 and aarch64; any other architecture will return. We also have a redundant llvm_unreachable later in the method if the architecture isn't one of these two.

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;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we get this from Target?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should, this is also pre-existing :)

Choose a reason for hiding this comment

The 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 const int addr_size = 8; at the top and use that instead of 8, or get it from the target's archspec.

Expand Down Expand Up @@ -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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this condition only relevant in the prologue?

Choose a reason for hiding this comment

The 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?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adrian-prantl indirect_context is also relevant below, I will comment there for visibility.

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
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adrian-prantl here's some other logic conditional on indirect_context, and here it's relevant both in and after the prologue.

// 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Choose a reason for hiding this comment

The 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 lldbutil.check_variable()

Copy link
Author

Choose a reason for hiding this comment

The 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)
4 changes: 2 additions & 2 deletions lldb/test/API/lang/swift/async/frame/variable/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ func randInt(_ i: Int) async -> Int {

func inner() async {
let a = await randInt(30)
let b = await randInt(a + 11)
use(a, b)
let b = await randInt(a + 11) // break one
use(a, b) // break two
}

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