Skip to content

[lldb][swift] Improve async stepping with arm64e #10671

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
Original file line number Diff line number Diff line change
Expand Up @@ -2663,20 +2663,48 @@ static llvm::Expected<addr_t> ReadAsyncContextRegisterFromUnwind(
return async_reg;
}

static llvm::Expected<bool>
DoesContinuationPointToSameFunction(addr_t async_reg, SymbolContext &sc,
Process &process) {
llvm::Expected<addr_t> continuation_ptr = ReadPtrFromAddr(
process, async_reg, /*offset*/ process.GetAddressByteSize());
if (!continuation_ptr)
return continuation_ptr.takeError();

Address continuation_addr;
continuation_addr.SetLoadAddress(process.FixCodeAddress(*continuation_ptr),
&process.GetTarget());
if (sc.function)
return sc.function->GetAddressRange().ContainsLoadAddress(
continuation_addr, &process.GetTarget());
assert(sc.symbol);
return sc.symbol->ContainsFileAddress(continuation_addr.GetFileAddress());
}

/// Returns true if the async register should be dereferenced once to obtain the
/// CFA of the currently executing function. This is the case at the start of
/// "Q" funclets, before the low level code changes the meaning of the async
/// register to not require the indirection.
/// The end of the prologue approximates the transition point.
/// The end of the prologue approximates the transition point well in non-arm64e
/// targets.
/// FIXME: In the few instructions between the end of the prologue and the
/// transition point, this approximation fails. rdar://139676623
static llvm::Expected<bool> IsIndirectContext(Process &process,
StringRef mangled_name,
Address pc, SymbolContext &sc) {
Address pc, SymbolContext &sc,
addr_t async_reg) {
if (!SwiftLanguageRuntime::IsSwiftAsyncAwaitResumePartialFunctionSymbol(
mangled_name))
return false;

// For arm64e, pointer authentication generates branches that cause stepping
// algorithms to stop & unwind in more places. The "end of the prologue"
// approximation fails in those; instead, check whether the continuation
// pointer still points to the currently executing function. This works for
// all instructions, but fails when direct recursion is involved.
if (process.GetTarget().GetArchitecture().GetTriple().isArm64e())
return DoesContinuationPointToSameFunction(async_reg, sc, process);

// This is checked prior to calling this function.
assert(sc.function || sc.symbol);
uint32_t prologue_size = sc.function ? sc.function->GetPrologueByteSize()
Expand Down Expand Up @@ -2759,7 +2787,7 @@ SwiftLanguageRuntime::GetRuntimeUnwindPlan(ProcessSP process_sp,
return log_expected(async_reg.takeError());

llvm::Expected<bool> maybe_indirect_context =
IsIndirectContext(*process_sp, mangled_name, pc, sc);
IsIndirectContext(*process_sp, mangled_name, pc, sc, *async_reg);
if (!maybe_indirect_context)
return log_expected(maybe_indirect_context.takeError());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,17 +378,7 @@ CreateRunThroughTaskSwitchThreadPlan(Thread &thread,
if (!resume_fn_ptr)
return {};

auto arch = reg_ctx->CalculateTarget()->GetArchitecture();
std::optional<AsyncUnwindRegisterNumbers> async_regs =
GetAsyncUnwindRegisterNumbers(arch.GetMachine());
if (!async_regs)
return {};
unsigned async_reg_number = reg_ctx->ConvertRegisterKindToRegisterNumber(
async_regs->GetRegisterKind(), async_regs->async_ctx_regnum);
uint64_t async_ctx = reg_ctx->ReadRegisterAsUnsigned(async_reg_number, 0);
if (!async_ctx)
return {};

resume_fn_ptr = thread.GetProcess()->FixCodeAddress(resume_fn_ptr);
return std::make_shared<ThreadPlanRunToAddress>(thread, resume_fn_ptr,
/*stop_others*/ false);
}
Expand Down
3 changes: 2 additions & 1 deletion lldb/source/Target/StackID.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ bool lldb_private::operator!=(const StackID &lhs, const StackID &rhs) {
static llvm::Expected<bool> IsReachableParent(lldb::addr_t source,
lldb::addr_t maybe_parent,
Process &process) {
maybe_parent = process.FixDataAddress(maybe_parent);
auto max_num_frames = 512;
for (lldb::addr_t parent_ctx = source; parent_ctx && max_num_frames;
max_num_frames--) {
Expand All @@ -94,7 +95,7 @@ static llvm::Expected<bool> IsReachableParent(lldb::addr_t source,
return llvm::createStringError(llvm::formatv(
"Failed to read parent async context of: {0:x}. Error: {1}",
old_parent_ctx, error.AsCString()));
if (parent_ctx == maybe_parent)
if (process.FixDataAddress(parent_ctx) == maybe_parent)
return true;
}
if (max_num_frames == 0)
Expand Down
46 changes: 24 additions & 22 deletions lldb/unittests/StackID/StackIDTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,25 +76,27 @@ struct MockStackID : StackID {
};

TEST_F(StackIDTest, StackStackCFAComparison) {
auto process = MockProcess(m_target_sp, Listener::MakeListener("dummy"));
Copy link
Author

@felipepiovezan felipepiovezan May 13, 2025

Choose a reason for hiding this comment

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

In case you're puzzled by this change: FixDataAddress indirectly calls "shared_from_this" on the process class, so it can't be stack allocated.

auto process = std::make_shared<MockProcess>(m_target_sp,
Listener::MakeListener("dummy"));

MockStackID small_cfa_on_stack(/*cfa*/ 10, OnStack::Yes);
MockStackID big_cfa_on_stack(/*cfa*/ 100, OnStack::Yes);

EXPECT_TRUE(
StackID::IsYounger(small_cfa_on_stack, big_cfa_on_stack, process));
StackID::IsYounger(small_cfa_on_stack, big_cfa_on_stack, *process));
EXPECT_FALSE(
StackID::IsYounger(big_cfa_on_stack, small_cfa_on_stack, process));
StackID::IsYounger(big_cfa_on_stack, small_cfa_on_stack, *process));
}

TEST_F(StackIDTest, StackHeapCFAComparison) {
auto process = MockProcess(m_target_sp, Listener::MakeListener("dummy"));
auto process = std::make_shared<MockProcess>(m_target_sp,
Listener::MakeListener("dummy"));

MockStackID cfa_on_stack(/*cfa*/ 100, OnStack::Yes);
MockStackID cfa_on_heap(/*cfa*/ 10, OnStack::No);

EXPECT_TRUE(StackID::IsYounger(cfa_on_stack, cfa_on_heap, process));
EXPECT_FALSE(StackID::IsYounger(cfa_on_heap, cfa_on_stack, process));
EXPECT_TRUE(StackID::IsYounger(cfa_on_stack, cfa_on_heap, *process));
EXPECT_FALSE(StackID::IsYounger(cfa_on_heap, cfa_on_stack, *process));
}

TEST_F(StackIDTest, HeapHeapCFAComparison) {
Expand All @@ -107,21 +109,21 @@ TEST_F(StackIDTest, HeapHeapCFAComparison) {
memory_map[100] = 108;
memory_map[108] = 116;
memory_map[116] = 0;
auto process = MockProcess(m_target_sp, Listener::MakeListener("dummy"),
std::move(memory_map));
auto process = std::make_shared<MockProcess>(
m_target_sp, Listener::MakeListener("dummy"), std::move(memory_map));

MockStackID oldest_cfa(/*cfa*/ 116, OnStack::No);
MockStackID middle_cfa(/*cfa*/ 108, OnStack::No);
MockStackID youngest_cfa(/*cfa*/ 100, OnStack::No);

EXPECT_TRUE(StackID::IsYounger(youngest_cfa, oldest_cfa, process));
EXPECT_FALSE(StackID::IsYounger(oldest_cfa, youngest_cfa, process));
EXPECT_TRUE(StackID::IsYounger(youngest_cfa, oldest_cfa, *process));
EXPECT_FALSE(StackID::IsYounger(oldest_cfa, youngest_cfa, *process));

EXPECT_TRUE(StackID::IsYounger(youngest_cfa, middle_cfa, process));
EXPECT_FALSE(StackID::IsYounger(middle_cfa, youngest_cfa, process));
EXPECT_TRUE(StackID::IsYounger(youngest_cfa, middle_cfa, *process));
EXPECT_FALSE(StackID::IsYounger(middle_cfa, youngest_cfa, *process));

EXPECT_TRUE(StackID::IsYounger(middle_cfa, oldest_cfa, process));
EXPECT_FALSE(StackID::IsYounger(oldest_cfa, middle_cfa, process));
EXPECT_TRUE(StackID::IsYounger(middle_cfa, oldest_cfa, *process));
EXPECT_FALSE(StackID::IsYounger(oldest_cfa, middle_cfa, *process));
}

TEST_F(StackIDTest, HeapHeapCFAComparisonDecreasing) {
Expand All @@ -134,19 +136,19 @@ TEST_F(StackIDTest, HeapHeapCFAComparisonDecreasing) {
memory_map[100] = 90;
memory_map[90] = 80;
memory_map[80] = 0;
auto process = MockProcess(m_target_sp, Listener::MakeListener("dummy"),
std::move(memory_map));
auto process = std::make_shared<MockProcess>(
m_target_sp, Listener::MakeListener("dummy"), std::move(memory_map));

MockStackID oldest_cfa(/*cfa*/ 80, OnStack::No);
MockStackID middle_cfa(/*cfa*/ 90, OnStack::No);
MockStackID youngest_cfa(/*cfa*/ 100, OnStack::No);

EXPECT_TRUE(StackID::IsYounger(youngest_cfa, oldest_cfa, process));
EXPECT_FALSE(StackID::IsYounger(oldest_cfa, youngest_cfa, process));
EXPECT_TRUE(StackID::IsYounger(youngest_cfa, oldest_cfa, *process));
EXPECT_FALSE(StackID::IsYounger(oldest_cfa, youngest_cfa, *process));

EXPECT_TRUE(StackID::IsYounger(youngest_cfa, middle_cfa, process));
EXPECT_FALSE(StackID::IsYounger(middle_cfa, youngest_cfa, process));
EXPECT_TRUE(StackID::IsYounger(youngest_cfa, middle_cfa, *process));
EXPECT_FALSE(StackID::IsYounger(middle_cfa, youngest_cfa, *process));

EXPECT_TRUE(StackID::IsYounger(middle_cfa, oldest_cfa, process));
EXPECT_FALSE(StackID::IsYounger(oldest_cfa, middle_cfa, process));
EXPECT_TRUE(StackID::IsYounger(middle_cfa, oldest_cfa, *process));
EXPECT_FALSE(StackID::IsYounger(oldest_cfa, middle_cfa, *process));
}