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

Conversation

kastiglione
Copy link

@kastiglione kastiglione commented Jun 6, 2022

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

@kastiglione kastiglione force-pushed the lldb-Use-consistent-CFA-before-after-prologue-of-async-functions branch from c67de06 to f76f320 Compare June 6, 2022 23:14
@kastiglione
Copy link
Author

@swift-ci test

@kastiglione kastiglione marked this pull request as draft June 6, 2022 23:28
Copy link

@adrian-prantl adrian-prantl left a comment

Choose a reason for hiding this comment

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

Nice!

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

prologue_size = sc.symbol->GetPrologueByteSize();
mangled_name = sc.symbol->GetMangled().GetMangledName();
} 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.

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.

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

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.

Copy link

@jasonmolenda jasonmolenda left a comment

Choose a reason for hiding this comment

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

LGTM.

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

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

prologue_size = sc.symbol->GetPrologueByteSize();
mangled_name = sc.symbol->GetMangled().GetMangledName();
} else {
return UnwindPlanSP();

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.

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

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.

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

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.

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.

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.

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?

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

@kastiglione kastiglione marked this pull request as ready for review June 7, 2022 18:37
sc.symbol->GetMangled().GetMangledName().GetStringRef())
: false;
if (in_prologue) {
if (indirect_context)
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.

Comment on lines 2557 to +2560
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 {
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.

@kastiglione
Copy link
Author

@swift-ci test

@kastiglione
Copy link
Author

This change uncovered a crash, which is why I haven't merged it yet. The crash is fixed by #4838

@kastiglione kastiglione merged commit be3823b into swift/release/5.7 Jul 6, 2022
@kastiglione kastiglione deleted the lldb-Use-consistent-CFA-before-after-prologue-of-async-functions branch July 6, 2022 18:12
kastiglione added a commit that referenced this pull request Jul 8, 2022
…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
(cherry picked from commit be3823b)
kastiglione added a commit that referenced this pull request Jul 9, 2022
…4806) (#4955)

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

(cherry picked from PR #4806)
kastiglione added a commit that referenced this pull request Jul 15, 2022
…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
(cherry-picked from commit be3823b)
kastiglione added a commit that referenced this pull request Jul 15, 2022
…4806) (#4955)

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

(cherry picked from PR #4806)


(cherry-picked from commit 6f9af03)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants