Skip to content

[lldb][swift] Use frame formation as a guide for async unwinding #10637

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

Open
wants to merge 1 commit into
base: swift/release/6.2
Choose a base branch
from

Conversation

felipepiovezan
Copy link

This patch changes how the swift language runtime obtains the async context of the currently executing function.

The old strategy consists of querying unwind plans for what they believe the location of the async register is. This proved problematic in cases where the register is not saved directly, e.g. in arm64e, where the register is signed and moved to a scratch register prior to being saved.

The new strategy relies on ABI guarantees: the async context is always saved at the stack slot immediately beneath where FP is saved. As long as a frame has been formed, we can always access the async context by reading memory @ [CFA-24] (FP is saved in [CFA-16]). Before the frame is formed, the async register can be accessed directly.

In other words, the main problem this patch has to solve is detecting the point at which a frame has been fully formed. This is accomplished by scanning the first few Rows of the assembly unwind plan, until a Row is found where FP's location is "AtCFAPlusOffset". The Row immediately after that is used. This is reliable because async prologues follow this pattern in all tested architectures (x86-64, arm64, arm64e):

// 0. adjust sp
// 1. save lr @ CFA-8
// 2. save fp @ CFA-16
// 3. save async_reg @ CFA-24 << "row immediately after"

For frameless functions, we can always use the async register directly.

While this patch adds no tests, all of the existing stepping tests and tests inspecting variables exercise the new behavior.

@felipepiovezan felipepiovezan requested a review from a team as a code owner May 7, 2025 22:36
@felipepiovezan
Copy link
Author

@swift-ci test

/// function, this offset is UINT_MAX.
/// 2. The CFA offset at which FP is stored.
static llvm::Expected<FrameSetupInfo>
GetFrameSetupInfo(UnwindPlan &unwind_plan, RegisterContext &regctx) {
Copy link
Author

Choose a reason for hiding this comment

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

@jasonmolenda this is the most important function of this patch.

@felipepiovezan
Copy link
Author

None of those are failing locally... investigating. CI builds with debug info, but it should not matter here...

@felipepiovezan
Copy link
Author

Ah I made the mistake of assuming the x86 plan would track FP always, like the arm plans do. @jasonmolenda had previously mentioned we have this inconsistency where "same" can be expressed both as a rule "same" and as the absence of a rule.

row[0]:    0: CFA=rsp +8 => rsp=CFA+0 rip=[CFA-8]
row[1]:    6: CFA=rsp+16 => rbp=[CFA-16] rsp=CFA+0 rip=[CFA-8]

@felipepiovezan felipepiovezan force-pushed the felipe/frame_formation_based_unwinding branch from 6355d5a to 781dccd Compare May 8, 2025 01:20
@felipepiovezan
Copy link
Author

@swift-ci test

@felipepiovezan
Copy link
Author

@swift-ci test windows platform

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.

All looks good to me.

/// Detect the point in the function where the prologue created a frame,
/// returning:
/// 1. The offset of the first instruction after that point. For a frameless
/// function, this offset is UINT_MAX.

Choose a reason for hiding this comment

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

It's UINT64_MAX.

Choose a reason for hiding this comment

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

(or, as we typically do for addr_t, LLDB_INVALID_ADDRESS, although we all know addr_t is just a UInt64)

Copy link
Author

Choose a reason for hiding this comment

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

I was trying to avoid LLDB_INVALID_ADDRESS because I wanted to make it clear that one can compare PC against this value with <. I'll spell it to make this clear.


// This is a frameless function, use an infinite offset to represent this.
if (it == fp_locs.end())
return FrameSetupInfo{std::numeric_limits<addr_t>::max(), 0};

Choose a reason for hiding this comment

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

Might be better to use UINT64_MAX here to match the description above.

Copy link
Author

Choose a reason for hiding this comment

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

This makes me a bit anxious, as this type could very well be int64_t (even though it isn't). So I wanted to be very explicit that this is a large positive number.
Let me tweak the comment.

// 2. save fp @ CFA-16 << `it` points to this row.
// 3. save async_reg @ CFA-24 << subsequent row.
// Use subsequent row, if available.
int row_idx = fp_locs.end() - it;

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 you're showing the instruction sequence or the rows here. A typical AArch64 prologue can be

sub	sp, sp, #0x20
stp	fp, lr, [sp, #0x10]
add	fp, sp, #0x10

If this is arm64e (ARMv8.3 ptrauth) codegen, there will be a pacibsp instruction to add auth bits to lr before it is spilled to stack.

Then from an unwind rule perspective we'll see

0th row: unwind state on function entry (pc=lr)
1st row: adjust sp (CFA is in terms of sp)
2nd row: store caller's fp & lr (CFA is in terms of sp)
3rd row: copy stack pointer + offset into fp reg  (CFA is in terms of fp)

So we'll identify row 2 (the third row) as the point where fp has been saved to stack at CFA+offset, in nearly all prologues.

I'm mostly not clear what the numbering in this comment is representing - instructions or unwind rows.

Copy link
Author

@felipepiovezan felipepiovezan May 9, 2025

Choose a reason for hiding this comment

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

I'm mostly not clear what the numbering in this comment is representing - instructions or unwind rows.

Neither! It was just meant to be: "this is roughly the order in which things happen". I'll update the comment to make this clear and remove numbers altogether

This patch changes how the swift language runtime obtains the async
context of the currently executing function.

The old strategy consists of querying unwind plans for what they believe
the location of the async register is. This proved problematic in cases
where the register is not saved directly, e.g. in arm64e, where the
register is signed and moved to a scratch register prior to being saved.

The new strategy relies on ABI guarantees: the async context is _always_
saved at the stack slot immediately beneath where FP is saved. As long
as a frame has been formed, we can always access the async context by
reading memory @ [CFA-24] (FP is saved in [CFA-16]). Before the frame is
formed, the async register can be accessed directly.

In other words, the main problem this patch has to solve is detecting
the point at which a frame has been fully formed. This is accomplished
by scanning the first few Rows of the assembly unwind plan, until a Row
is found where FP's location is "AtCFAPlusOffset". The Row immediately
after that is used. This is reliable because async prologues follow this
pattern in all tested architectures (x86-64, arm64, arm64e):

// 0. adjust sp
// 1. save lr        @ CFA-8
// 2. save fp        @ CFA-16
// 3. save async_reg @ CFA-24  << "row immediately after"

For frameless functions, we can always use the async register directly.

While this patch adds no tests, all of the existing stepping tests and
tests inspecting variables exercise the new behavior.
@felipepiovezan felipepiovezan force-pushed the felipe/frame_formation_based_unwinding branch from 781dccd to d1ac271 Compare May 9, 2025 20:19
@felipepiovezan
Copy link
Author

@swift-ci test

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.

2 participants