Skip to content

Commit 312cf91

Browse files
[lldb][swift] Use assembly unwinders to recover async registers
The SwiftLanguageRuntime unwind plan for async functions relies on the extended frame setup to recover the asynchronous contexts of these functions. This extended frame consists of: 1. Tagging the saved fp register by setting its 60 bit, indicating the existence of the extended frame. 2. An additional stack slot where the asynchronous context is saved. The current unwind plans recover the asynchronous context by reading the stack slot described in (2). This creates a number of challenges: * The extended frame is not always created (e.g. leaf funclets). * The extended frame is not necessarily valid inside the prologue or epilogue. * The code to handle all possible combinations of funclet x {is_prologue, is_epilogue} is very complex. * Entry funclets don't have the extended frame. This patch uses a new approach, exploiting the property that the asynchronous context is passed in a callee-saved register. We use existing unwind plans that inspect assembly and track spill slots, and recover the asynchronous register from those. Unwind Plans are not setup to query other Unwind Plans, more specifically they are not setup to materialize the abstract location provided by an Unwind Plan into a concrete value. Doing so would require a lot of plumbing. Instead, this patch reimplements a very targeted conversion between the abstract locations and concrete register values. We argue this is worth it, as the new unwind procedure is much simpler, relies on existing plans, and works in many cases where the previous implementation doesn't. Testing-wise, this commit creates a fairly complex async function, with multiple split points, and sets breakpoints on all instructions of all funclets. It then asserts that unwinding works correctly in all of them. This has revealed an issue with Q funclets, which clobber the async context location prior to freeing the context of the funclet that has just finished executing (see the FIXME in the test). This is not a problem with the new approach, but rather it was discovered because of the extensive testing done here.
1 parent 1815d07 commit 312cf91

File tree

6 files changed

+324
-124
lines changed

6 files changed

+324
-124
lines changed

lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntime.cpp

Lines changed: 136 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@
3737
#include "lldb/Interpreter/CommandObject.h"
3838
#include "lldb/Interpreter/CommandObjectMultiword.h"
3939
#include "lldb/Interpreter/CommandReturnObject.h"
40+
#include "lldb/Symbol/FuncUnwinders.h"
4041
#include "lldb/Symbol/Function.h"
4142
#include "lldb/Symbol/VariableList.h"
4243
#include "lldb/Target/RegisterContext.h"
44+
#include "lldb/Target/UnwindLLDB.h"
4345
#include "lldb/Utility/LLDBLog.h"
4446
#include "lldb/Utility/Log.h"
4547
#include "lldb/Utility/OptionParsing.h"
@@ -2584,37 +2586,111 @@ lldb::addr_t SwiftLanguageRuntime::GetAsyncContext(RegisterContext *regctx) {
25842586
return LLDB_INVALID_ADDRESS;
25852587
}
25862588

2587-
/// Creates an expression accessing *(fp - 8) or **(fp - 8) if
2588-
/// `with_double_deref` is true. This is only valid for x86_64 or aarch64.
2589-
llvm::ArrayRef<uint8_t>
2590-
GetAsyncRegFromFramePointerDWARFExpr(llvm::Triple::ArchType triple,
2591-
bool with_double_deref) {
2592-
assert(triple == llvm::Triple::x86_64 || triple == llvm::Triple::aarch64);
2593-
2594-
// These expressions must have static storage, due to how UnwindPlan::Row
2595-
// works.
2596-
static const uint8_t g_cfa_dwarf_expression_x86_64[] = {
2597-
llvm::dwarf::DW_OP_breg6, // DW_OP_breg6, register 6 == rbp
2598-
0x78, // sleb128 -8 (ptrsize)
2599-
llvm::dwarf::DW_OP_deref,
2600-
llvm::dwarf::DW_OP_deref,
2601-
};
2602-
static const uint8_t g_cfa_dwarf_expression_arm64[] = {
2603-
llvm::dwarf::DW_OP_breg29, // DW_OP_breg29, register 29 == fp
2604-
0x78, // sleb128 -8 (ptrsize)
2605-
llvm::dwarf::DW_OP_deref,
2606-
llvm::dwarf::DW_OP_deref,
2607-
};
2589+
/// Functional wrapper to read a register as an address.
2590+
static std::optional<addr_t> ReadRegisterAsAddress(RegisterContext &regctx,
2591+
unsigned regnum) {
2592+
auto reg = regctx.ReadRegisterAsUnsigned(regnum, LLDB_INVALID_ADDRESS);
2593+
if (reg != LLDB_INVALID_ADDRESS)
2594+
return reg;
2595+
return {};
2596+
}
2597+
2598+
/// Functional wrapper to read a pointer from process memory at `addr +
2599+
/// offset`.
2600+
static std::optional<addr_t>
2601+
ReadPtrFromAddr(Process &process, std::optional<addr_t> addr, int offset = 0) {
2602+
if (!addr)
2603+
return {};
2604+
Status error;
2605+
addr_t ptr = process.ReadPointerFromMemory(*addr + offset, error);
2606+
if (ptr != LLDB_INVALID_ADDRESS)
2607+
return ptr;
2608+
return {};
2609+
}
26082610

2609-
const uint8_t *expr = triple == llvm::Triple::x86_64
2610-
? g_cfa_dwarf_expression_x86_64
2611-
: g_cfa_dwarf_expression_arm64;
2612-
auto size = triple == llvm::Triple::x86_64
2613-
? sizeof(g_cfa_dwarf_expression_x86_64)
2614-
: sizeof(g_cfa_dwarf_expression_arm64);
2615-
if (with_double_deref)
2616-
return llvm::ArrayRef<uint8_t>(expr, size);
2617-
return llvm::ArrayRef<uint8_t>(expr, size - 1);
2611+
/// Computes the Canonical Frame Address (CFA) by converting the abstract
2612+
/// location of UnwindPlan::Row::FAValue into a concrete address. This is a
2613+
/// simplified version of the methods in RegisterContextUnwind, since plumbing
2614+
/// access to those here would be challenging.
2615+
static std::optional<addr_t> GetCFA(Process &process, RegisterContext &regctx,
2616+
UnwindPlan::Row::FAValue cfa_loc) {
2617+
using ValueType = UnwindPlan::Row::FAValue::ValueType;
2618+
switch (cfa_loc.GetValueType()) {
2619+
case ValueType::isRegisterPlusOffset: {
2620+
unsigned regnum = cfa_loc.GetRegisterNumber();
2621+
if (std::optional<addr_t> regvalue = ReadRegisterAsAddress(regctx, regnum))
2622+
return *regvalue + cfa_loc.GetOffset();
2623+
break;
2624+
}
2625+
case ValueType::isConstant:
2626+
case ValueType::isDWARFExpression:
2627+
case ValueType::isRaSearch:
2628+
case ValueType::isRegisterDereferenced:
2629+
case ValueType::unspecified:
2630+
break;
2631+
}
2632+
return {};
2633+
}
2634+
2635+
/// Attempts to use UnwindPlans that inspect assembly to recover the entry value
2636+
/// of the async context register. This is a simplified version of the methods
2637+
/// in RegisterContextUnwind, since plumbing access to those here would be
2638+
/// challenging.
2639+
static std::optional<addr_t> ReadAsyncContextRegisterFromUnwind(
2640+
SymbolContext &sc, Process &process, Address pc, Address func_start_addr,
2641+
RegisterContext &regctx, AsyncUnwindRegisterNumbers regnums) {
2642+
FuncUnwindersSP unwinders =
2643+
pc.GetModule()->GetUnwindTable().GetFuncUnwindersContainingAddress(pc,
2644+
sc);
2645+
if (!unwinders)
2646+
return {};
2647+
2648+
Target &target = process.GetTarget();
2649+
UnwindPlanSP unwind_plan =
2650+
unwinders->GetUnwindPlanAtNonCallSite(target, regctx.GetThread());
2651+
if (!unwind_plan)
2652+
return {};
2653+
2654+
UnwindPlan::RowSP row = unwind_plan->GetRowForFunctionOffset(
2655+
pc.GetFileAddress() - func_start_addr.GetFileAddress());
2656+
UnwindPlan::Row::AbstractRegisterLocation regloc;
2657+
2658+
// If the plan doesn't have information about the async register, we can use
2659+
// its current value, as this is a callee saved register.
2660+
if (!row->GetRegisterInfo(regnums.async_ctx_regnum, regloc))
2661+
return ReadRegisterAsAddress(regctx, regnums.async_ctx_regnum);
2662+
2663+
// Handle the few abstract locations we are likely to encounter.
2664+
using RestoreType = UnwindPlan::Row::AbstractRegisterLocation::RestoreType;
2665+
RestoreType loctype = regloc.GetLocationType();
2666+
switch (loctype) {
2667+
case RestoreType::same:
2668+
case RestoreType::inOtherRegister: {
2669+
unsigned regnum = loctype == RestoreType::same ? regnums.async_ctx_regnum
2670+
: regloc.GetRegisterNumber();
2671+
return ReadRegisterAsAddress(regctx, regnum);
2672+
}
2673+
case RestoreType::atCFAPlusOffset: {
2674+
std::optional<addr_t> cfa = GetCFA(process, regctx, row->GetCFAValue());
2675+
return ReadPtrFromAddr(process, cfa, regloc.GetOffset());
2676+
}
2677+
case RestoreType::isCFAPlusOffset: {
2678+
std::optional<addr_t> cfa = GetCFA(process, regctx, row->GetCFAValue());
2679+
if (!cfa)
2680+
return {};
2681+
return *cfa + regloc.GetOffset();
2682+
}
2683+
case RestoreType::isConstant:
2684+
return regloc.GetConstant();
2685+
case RestoreType::unspecified:
2686+
case RestoreType::undefined:
2687+
case RestoreType::atAFAPlusOffset:
2688+
case RestoreType::isAFAPlusOffset:
2689+
case RestoreType::isDWARFExpression:
2690+
case RestoreType::atDWARFExpression:
2691+
return {};
2692+
}
2693+
return {};
26182694
}
26192695

26202696
// Examine the register state and detect the transition from a real
@@ -2644,12 +2720,6 @@ SwiftLanguageRuntime::GetRuntimeUnwindPlan(ProcessSP process_sp,
26442720
return UnwindPlanSP();
26452721
}
26462722

2647-
// If we're in the prologue of a function, don't provide a Swift async
2648-
// unwind plan. We can be tricked by unmodified caller-registers that
2649-
// make this look like an async frame when this is a standard ABI function
2650-
// call, and the parent is the async frame.
2651-
// This assumes that the frame pointer register will be modified in the
2652-
// prologue.
26532723
Address pc;
26542724
pc.SetLoadAddress(regctx->GetPC(), &target);
26552725
SymbolContext sc;
@@ -2659,111 +2729,56 @@ SwiftLanguageRuntime::GetRuntimeUnwindPlan(ProcessSP process_sp,
26592729
return UnwindPlanSP();
26602730

26612731
Address func_start_addr;
2662-
uint32_t prologue_size;
26632732
ConstString mangled_name;
26642733
if (sc.function) {
26652734
func_start_addr = sc.function->GetAddressRange().GetBaseAddress();
2666-
prologue_size = sc.function->GetPrologueByteSize();
26672735
mangled_name = sc.function->GetMangled().GetMangledName();
26682736
} else if (sc.symbol) {
26692737
func_start_addr = sc.symbol->GetAddress();
2670-
prologue_size = sc.symbol->GetPrologueByteSize();
26712738
mangled_name = sc.symbol->GetMangled().GetMangledName();
26722739
} else {
26732740
return UnwindPlanSP();
26742741
}
26752742

2676-
AddressRange prologue_range(func_start_addr, prologue_size);
2677-
bool in_prologue = (func_start_addr == pc ||
2678-
prologue_range.ContainsLoadAddress(pc, &target));
2679-
2680-
if (in_prologue) {
2681-
if (!IsAnySwiftAsyncFunctionSymbol(mangled_name.GetStringRef()))
2682-
return UnwindPlanSP();
2683-
} else {
2684-
addr_t saved_fp = LLDB_INVALID_ADDRESS;
2685-
Status error;
2686-
if (!process_sp->ReadMemory(fp, &saved_fp, 8, error))
2687-
return UnwindPlanSP();
2688-
2689-
// Get the high nibble of the dreferenced fp; if the 60th bit is set,
2690-
// this is the transition to a swift async AsyncContext chain.
2691-
if ((saved_fp & (0xfULL << 60)) >> 60 != 1)
2692-
return UnwindPlanSP();
2693-
}
2743+
if (!IsAnySwiftAsyncFunctionSymbol(mangled_name.GetStringRef()))
2744+
return UnwindPlanSP();
26942745

2695-
// The coroutine funclets split from an async function have 2 different ABIs:
2696-
// - Async suspend partial functions and the first funclet get their async
2697-
// context directly in the async register.
2698-
// - Async await resume partial functions take their context indirectly, it
2699-
// needs to be dereferenced to get the actual function's context.
2700-
// The debug info for locals reflects this difference, so our unwinding of the
2701-
// context register needs to reflect it too.
2746+
// The async register contains, at the start of the funclet:
2747+
// 1. The async context of the async function that just finished executing,
2748+
// for await resume ("Q") funclets ("indirect context").
2749+
// 2. The async context for the currently executing async function, for all
2750+
// other funclets ("Y" and "Yx" funclets, where "x" is a number).
27022751
bool indirect_context =
27032752
IsSwiftAsyncAwaitResumePartialFunctionSymbol(mangled_name.GetStringRef());
27042753

2754+
std::optional<addr_t> async_reg = ReadAsyncContextRegisterFromUnwind(
2755+
sc, *process_sp, pc, func_start_addr, *regctx, *regnums);
2756+
std::optional<addr_t> async_ctx =
2757+
indirect_context ? ReadPtrFromAddr(*m_process, async_reg) : async_reg;
2758+
if (!async_reg || !async_ctx)
2759+
return UnwindPlanSP();
2760+
27052761
UnwindPlan::RowSP row(new UnwindPlan::Row);
27062762
const int32_t ptr_size = 8;
27072763
row->SetOffset(0);
27082764

2709-
if (in_prologue) {
2710-
if (indirect_context)
2711-
row->GetCFAValue().SetIsRegisterDereferenced(regnums->async_ctx_regnum);
2712-
else
2713-
row->GetCFAValue().SetIsRegisterPlusOffset(regnums->async_ctx_regnum, 0);
2714-
} else {
2715-
// In indirect funclets, dereferencing (fp-8) once produces the CFA of the
2716-
// frame above. Dereferencing twice will produce the current frame's CFA.
2717-
bool with_double_deref = indirect_context;
2718-
llvm::ArrayRef<uint8_t> expr = GetAsyncRegFromFramePointerDWARFExpr(
2719-
arch.GetMachine(), with_double_deref);
2720-
row->GetCFAValue().SetIsDWARFExpression(expr.data(), expr.size());
2721-
}
2722-
2723-
if (indirect_context) {
2724-
if (in_prologue) {
2725-
row->SetRegisterLocationToSame(regnums->async_ctx_regnum, false);
2726-
} else {
2727-
llvm::ArrayRef<uint8_t> expr = GetAsyncRegFromFramePointerDWARFExpr(
2728-
arch.GetMachine(), false /*with_double_deref*/);
2729-
row->SetRegisterLocationToIsDWARFExpression(
2730-
regnums->async_ctx_regnum, expr.data(), expr.size(), false);
2731-
}
2732-
} else {
2733-
// In the first part of a split async function, the context is passed
2734-
// directly, so we can use the CFA value directly.
2735-
row->SetRegisterLocationToIsCFAPlusOffset(regnums->async_ctx_regnum, 0,
2736-
false);
2737-
// The fact that we are in this case needs to be communicated to the frames
2738-
// below us as they need to react differently. There is no good way to
2739-
// expose this, so we set another dummy register to communicate this state.
2740-
static const uint8_t g_dummy_dwarf_expression[] = {
2741-
llvm::dwarf::DW_OP_const1u, 0
2742-
};
2743-
row->SetRegisterLocationToIsDWARFExpression(
2744-
regnums->dummy_regnum, g_dummy_dwarf_expression,
2745-
sizeof(g_dummy_dwarf_expression), false);
2746-
}
2747-
2748-
std::optional<addr_t> pc_after_prologue = [&]() -> std::optional<addr_t> {
2749-
// In the prologue, use the async_reg as is, it has not been clobbered.
2750-
if (in_prologue)
2751-
return TrySkipVirtualParentProlog(GetAsyncContext(regctx), *process_sp,
2752-
indirect_context);
2753-
2754-
// Both ABIs (x86_64 and aarch64) guarantee the async reg is saved at:
2755-
// *(fp - 8).
2756-
Status error;
2757-
addr_t async_reg_entry_value = LLDB_INVALID_ADDRESS;
2758-
process_sp->ReadMemory(fp - ptr_size, &async_reg_entry_value, ptr_size,
2759-
error);
2760-
if (error.Fail())
2761-
return {};
2762-
return TrySkipVirtualParentProlog(async_reg_entry_value, *process_sp,
2763-
indirect_context);
2764-
}();
2765+
// The CFA of a funclet is its own async context.
2766+
row->GetCFAValue().SetIsConstant(*async_ctx);
2767+
2768+
// The value of the async register in the parent frame is the entry value of
2769+
// the async register in the current frame. This mimics a function call, as
2770+
// if the parent funclet had called the current funclet.
2771+
row->SetRegisterLocationToIsConstant(regnums->async_ctx_regnum, *async_reg,
2772+
/*can_replace=*/false);
2773+
2774+
// The parent frame needs to know how to interpret the value it is given for
2775+
// its own async register. A dummy register is used to communicate that.
2776+
if (!indirect_context)
2777+
row->SetRegisterLocationToIsConstant(regnums->dummy_regnum, 0,
2778+
/*can_replace=*/false);
27652779

2766-
if (pc_after_prologue)
2780+
if (std::optional<addr_t> pc_after_prologue =
2781+
TrySkipVirtualParentProlog(*async_ctx, *process_sp))
27672782
row->SetRegisterLocationToIsConstant(regnums->pc_regnum, *pc_after_prologue,
27682783
false);
27692784
else

lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntime.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ class SwiftLanguageRuntime : public LanguageRuntime {
496496
/// address of the first non-prologue instruction.
497497
std::optional<lldb::addr_t>
498498
TrySkipVirtualParentProlog(lldb::addr_t async_reg_val, Process &process,
499-
unsigned num_indirections);
499+
unsigned num_indirections = 0);
500500
};
501501

502502
} // namespace lldb_private

lldb/test/API/lang/swift/async/stepping/step-in/task-switch/TestSwiftTaskSwitch.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
class TestCase(lldbtest.TestBase):
88
@swiftTest
99
@skipIf(oslist=["windows", "linux"])
10-
@skipIf(bugnumber="rdar://136083188")
1110
def test(self):
1211
"""Test conditions for async step-in."""
1312
self.build()
@@ -28,7 +27,7 @@ def test(self):
2827
# Using the line table, build a set of the non-zero line numbers for
2928
# this this function - and verify that there is exactly one line.
3029
lines = {inst.addr.line_entry.line for inst in instructions}
31-
lines.remove(0)
30+
lines.discard(0)
3231
self.assertEqual(lines, {3})
3332

3433
# Required for builds that have debug info.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
SWIFT_SOURCES := main.swift
2+
SWIFTFLAGS_EXTRAS := -parse-as-library
3+
include Makefile.rules

0 commit comments

Comments
 (0)