Skip to content

[lldb][swift] Evaluate entry_value(async_reg) in terms of CFA #9403

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
109 changes: 76 additions & 33 deletions lldb/source/Expression/DWARFExpression.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,74 @@ bool DWARFExpression::LinkThreadLocalStorage(
return true;
}

/// Returns true if \c opcodes contains the opcode for the register used for the
/// Swift Async Context (x22 for aarch64, r14 for x86-64). It must also not
/// contain any other opcodes.
static bool IsAsyncRegCtxExpr(DataExtractor &opcodes,
llvm::Triple::ArchType triple) {
offset_t offset = 0;
const uint8_t op_async_ctx_reg = opcodes.GetU8(&offset);
if (opcodes.BytesLeft(offset) != 0)
return false;

// FIXME: could this be exposed through the ABI/Language plugins?
return (triple == llvm::Triple::x86_64 && op_async_ctx_reg == DW_OP_reg14) ||
(triple == llvm::Triple::aarch64 && op_async_ctx_reg == DW_OP_reg22);

Choose a reason for hiding this comment

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

We should at least throw in a FIXME here: I feel like this should be a method of the ABI plugin. No idea how to get from here to the ABI of the Process.

Copy link
Author

Choose a reason for hiding this comment

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

Done

}

/// If \c opcodes contain the location of asynchronous contexts in Swift,
/// evaluates DW_OP_call_frame_cfa, returns its result, and updates
/// \c current_offset. Otherwise, does nothing. This is possible because, for
/// async frames, the language unwinder treats the asynchronous context as the
/// CFA of the frame.
static llvm::Expected<Value> SwiftAsyncEvaluate_DW_OP_entry_value(

Choose a reason for hiding this comment

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

Idea: In the future, languages could be able to register a DWARF expression plugin that matches sequences like this and returns a value?

Copy link
Author

@felipepiovezan felipepiovezan Oct 15, 2024

Choose a reason for hiding this comment

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

Yup, that could be an interesting idea, though I wonder how many languages would use this...

ExecutionContext &exe_ctx, StackFrame &current_frame,
const DWARFUnit *dwarf_cu, Function &func, const DataExtractor &opcodes,
offset_t &current_offset) {
auto func_name = func.GetMangled().GetMangledName();
if (!SwiftLanguageRuntime::IsAnySwiftAsyncFunctionSymbol(func_name))
return llvm::createStringError(
"SwiftAsyncEvaluate_DW_OP_entry_value: not an async function");

offset_t new_offset = current_offset;
const uint32_t subexpr_len = opcodes.GetULEB128(&new_offset);
const void *subexpr_data = opcodes.GetData(&new_offset, subexpr_len);
if (!subexpr_data)
return llvm::createStringError(
"SwiftAsyncEvaluate_DW_OP_entry_value: failed to extract subexpr");

DataExtractor subexpr_extractor(
subexpr_data, subexpr_len, opcodes.GetByteOrder(),
opcodes.GetAddressByteSize(), opcodes.getTargetByteSize());
if (!IsAsyncRegCtxExpr(subexpr_extractor,
exe_ctx.GetTargetRef().GetArchitecture().GetMachine()))
return llvm::createStringError("SwiftAsyncEvaluate_DW_OP_entry_value: "
"missing async context register opcode");

// Q funclets require an extra level of indirection.
if (SwiftLanguageRuntime::IsSwiftAsyncAwaitResumePartialFunctionSymbol(
func_name)) {
const uint8_t maybe_op_deref = opcodes.GetU8(&new_offset);
if (maybe_op_deref != DW_OP_deref)
return llvm::createStringError("SwiftAsyncEvaluate_DW_OP_entry_value: "
"missing DW_OP_deref in Q funclet");
}

static const uint8_t cfa_opcode = DW_OP_call_frame_cfa;
DataExtractor cfa_expr_data(&cfa_opcode, 1, opcodes.GetByteOrder(),
opcodes.GetAddressByteSize(),
opcodes.getTargetByteSize());
DWARFExpressionList cfa_expr(func.CalculateSymbolContextModule(),
cfa_expr_data, dwarf_cu);
llvm::Expected<Value> maybe_result = cfa_expr.Evaluate(
&exe_ctx, current_frame.GetRegisterContext().get(), LLDB_INVALID_ADDRESS,
/*initial_value_ptr=*/nullptr,
/*object_address_ptr=*/nullptr);
if (maybe_result)
current_offset = new_offset;
return maybe_result;
}

static llvm::Error
Evaluate_DW_OP_entry_value(std::vector<Value> &stack, const DWARFUnit *dwarf_cu,
ExecutionContext *exe_ctx, RegisterContext *reg_ctx,
Expand Down Expand Up @@ -629,20 +697,19 @@ Evaluate_DW_OP_entry_value(std::vector<Value> &stack, const DWARFUnit *dwarf_cu,
if (!current_func)
return llvm::createStringError("no current function");

if (llvm::Expected<Value> result = SwiftAsyncEvaluate_DW_OP_entry_value(
*exe_ctx, *current_frame, dwarf_cu, *current_func, opcodes,
opcode_offset)) {
stack.push_back(*result);
return llvm::Error::success();
} else
LLDB_LOG_ERROR(log, result.takeError(), "{0}");

CallEdge *call_edge = nullptr;
ModuleList &modlist = target.GetImages();
ExecutionContext parent_exe_ctx = *exe_ctx;
parent_exe_ctx.SetFrameSP(parent_frame);
Function *parent_func = nullptr;
#ifdef LLDB_ENABLE_SWIFT
// Swift async function arguments are represented relative to a
// DW_OP_entry_value that fetches the async context register. This
// register is known to the unwinder and can always be restored
// therefore it is not necessary to match up a call site parameter
// with it.
auto fn_name = current_func->GetMangled().GetMangledName().GetStringRef();
if (!SwiftLanguageRuntime::IsAnySwiftAsyncFunctionSymbol(fn_name)) {
#endif

parent_func =
parent_frame->GetSymbolContext(eSymbolContextFunction).function;
Expand Down Expand Up @@ -677,25 +744,16 @@ Evaluate_DW_OP_entry_value(std::vector<Value> &stack, const DWARFUnit *dwarf_cu,
if (!call_edge)
return llvm::createStringError("no unambiguous edge from parent "
"to current function");
#ifdef LLDB_ENABLE_SWIFT
}
#endif

// 3. Attempt to locate the DW_OP_entry_value expression in the set of
// available call site parameters. If found, evaluate the corresponding
// parameter in the context of the parent frame.
const uint32_t subexpr_len = opcodes.GetULEB128(&opcode_offset);
#ifdef LLDB_ENABLE_SWIFT
lldb::offset_t subexpr_offset = opcode_offset;
#endif
const void *subexpr_data = opcodes.GetData(&opcode_offset, subexpr_len);
if (!subexpr_data)
return llvm::createStringError("subexpr could not be read");

const CallSiteParameter *matched_param = nullptr;
#ifdef LLDB_ENABLE_SWIFT
if (call_edge) {
#endif
for (const CallSiteParameter &param : call_edge->GetCallSiteParameters()) {
DataExtractor param_subexpr_extractor;
if (!param.LocationInCallee.GetExpressionData(param_subexpr_extractor))
Expand All @@ -721,25 +779,10 @@ Evaluate_DW_OP_entry_value(std::vector<Value> &stack, const DWARFUnit *dwarf_cu,
}
if (!matched_param)
return llvm::createStringError("no matching call site param found");
#ifdef LLDB_ENABLE_SWIFT
}
std::optional<DWARFExpressionList> subexpr;
if (!matched_param) {
auto *ctx_func = parent_func ? parent_func : current_func;
subexpr.emplace(ctx_func->CalculateSymbolContextModule(),
DataExtractor(opcodes, subexpr_offset, subexpr_len),
dwarf_cu);
}
#endif

// TODO: Add support for DW_OP_push_object_address within a DW_OP_entry_value
// subexpresion whenever llvm does.
#ifdef LLDB_ENABLE_SWIFT
const DWARFExpressionList &param_expr =
matched_param ? matched_param->LocationInCaller : *subexpr;
#else
const DWARFExpressionList &param_expr = matched_param->LocationInCaller;
#endif

llvm::Expected<Value> maybe_result = param_expr.Evaluate(
&parent_exe_ctx, parent_frame->GetRegisterContext().get(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2539,11 +2539,6 @@ struct AsyncUnwindRegisterNumbers {
uint32_t async_ctx_regnum;
uint32_t fp_regnum;
uint32_t pc_regnum;
/// A register to use as a marker to indicate how the async context is passed
/// to the function (indirectly, or not). This needs to be communicated to the
/// frames below us as they need to react differently. There is no good way to
/// expose this, so we set another dummy register to communicate this state.
uint32_t dummy_regnum;

RegisterKind GetRegisterKind() const { return lldb::eRegisterKindDWARF; }
};
Expand All @@ -2557,15 +2552,13 @@ GetAsyncUnwindRegisterNumbers(llvm::Triple::ArchType triple) {
regnums.async_ctx_regnum = dwarf_r14_x86_64;
regnums.fp_regnum = dwarf_rbp_x86_64;
regnums.pc_regnum = dwarf_rip_x86_64;
regnums.dummy_regnum = dwarf_r15_x86_64;
return regnums;
}
case llvm::Triple::aarch64: {
AsyncUnwindRegisterNumbers regnums;
regnums.async_ctx_regnum = arm64_dwarf::x22;
regnums.fp_regnum = arm64_dwarf::fp;
regnums.pc_regnum = arm64_dwarf::pc;
regnums.dummy_regnum = arm64_dwarf::x23;
return regnums;
}
default:
Expand Down Expand Up @@ -2811,18 +2804,11 @@ SwiftLanguageRuntime::GetRuntimeUnwindPlan(ProcessSP process_sp,
// The CFA of a funclet is its own async context.
row->GetCFAValue().SetIsConstant(*async_ctx);

// The value of the async register in the parent frame is the entry value of
// the async register in the current frame. This mimics a function call, as
// if the parent funclet had called the current funclet.
row->SetRegisterLocationToIsConstant(regnums->async_ctx_regnum, *async_reg,
// The value of the async register in the parent frame (which is the
// continuation funclet) is the async context of this frame.
row->SetRegisterLocationToIsConstant(regnums->async_ctx_regnum, *async_ctx,
/*can_replace=*/false);

// The parent frame needs to know how to interpret the value it is given for
// its own async register. A dummy register is used to communicate that.
if (!indirect_context)
row->SetRegisterLocationToIsConstant(regnums->dummy_regnum, 0,
/*can_replace=*/false);

if (std::optional<addr_t> pc_after_prologue =
TrySkipVirtualParentProlog(*async_ctx, *process_sp))
row->SetRegisterLocationToIsConstant(regnums->pc_regnum, *pc_after_prologue,
Expand Down Expand Up @@ -2855,59 +2841,13 @@ UnwindPlanSP SwiftLanguageRuntime::GetFollowAsyncContextUnwindPlan(
if (!regnums)
return UnwindPlanSP();

const bool is_indirect =
regctx->ReadRegisterAsUnsigned(regnums->dummy_regnum, (uint64_t)-1ll) ==
(uint64_t)-1ll;
// In the general case, the async register setup by the frame above us
// should be dereferenced twice to get our context, except when the frame
// above us is an async frame on the OS stack that takes its context directly
// (see discussion in GetRuntimeUnwindPlan()). The availability of
// dummy_regnum is used as a marker for this situation.
if (!is_indirect) {
row->GetCFAValue().SetIsRegisterDereferenced(regnums->async_ctx_regnum);
row->SetRegisterLocationToSame(regnums->async_ctx_regnum, false);
} else {
static const uint8_t async_dwarf_expression_x86_64[] = {
llvm::dwarf::DW_OP_regx, dwarf_r14_x86_64, // DW_OP_regx, reg
llvm::dwarf::DW_OP_deref, // DW_OP_deref
llvm::dwarf::DW_OP_deref, // DW_OP_deref
};
static const uint8_t async_dwarf_expression_arm64[] = {
llvm::dwarf::DW_OP_regx, arm64_dwarf::x22, // DW_OP_regx, reg
llvm::dwarf::DW_OP_deref, // DW_OP_deref
llvm::dwarf::DW_OP_deref, // DW_OP_deref
};

const unsigned expr_size = sizeof(async_dwarf_expression_x86_64);
static_assert(sizeof(async_dwarf_expression_x86_64) ==
sizeof(async_dwarf_expression_arm64),
"Expressions of different sizes");

const uint8_t *expression = nullptr;
if (arch.GetMachine() == llvm::Triple::x86_64)
expression = async_dwarf_expression_x86_64;
else if (arch.GetMachine() == llvm::Triple::aarch64)
expression = async_dwarf_expression_arm64;
else
llvm_unreachable("Unsupported architecture");

// Note how the register location gets the same expression pointer with a
// different size. We just skip the trailing deref for it.
assert(expression[expr_size - 1] == llvm::dwarf::DW_OP_deref &&
"Should skip a deref");
row->GetCFAValue().SetIsDWARFExpression(expression, expr_size);
row->SetRegisterLocationToIsDWARFExpression(
regnums->async_ctx_regnum, expression, expr_size - 1, false);
}

// Suppose this is unwinding frame #2 of a call stack. The value given for
// the async register has two possible values, depending on what frame #1
// expects:
// 1. The CFA of frame #1, direct ABI, dereferencing it once produces CFA of
// Frame #2.
// 2. The CFA of frame #0, indirect ABI, dereferencing it twice produces CFA
// of Frame #2.
const unsigned num_indirections = 1 + is_indirect;
row->GetCFAValue().SetIsRegisterDereferenced(regnums->async_ctx_regnum);
// The value of the async register in the parent frame (which is the
// continuation funclet) is the async context of this frame.
row->SetRegisterLocationToIsCFAPlusOffset(regnums->async_ctx_regnum,
/*offset*/ 0, false);

const unsigned num_indirections = 1;
if (std::optional<addr_t> pc_after_prologue = TrySkipVirtualParentProlog(
GetAsyncContext(regctx), *process_sp, num_indirections))
row->SetRegisterLocationToIsConstant(regnums->pc_regnum, *pc_after_prologue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,63 @@
import lldbsuite.test.lldbutil as lldbutil


@skipIf(archs=no_match(["arm64", "arm64e", "x86_64"]))
class TestCase(lldbtest.TestBase):

mydir = lldbtest.TestBase.compute_mydir(__file__)

def read_ptr_from_memory(self, process, addr):
error = lldb.SBError()
value = process.ReadPointerFromMemory(addr, error)
self.assertSuccess(error, "Failed to read memory")
return value

# Check that the CFA chain is correctly built
def check_cfas(self, async_frames, process):
async_cfas = list(map(lambda frame: frame.GetCFA(), async_frames))
expected_cfas = [async_cfas[0]]
# The CFA chain ends in nullptr.
while expected_cfas[-1] != 0:
error = lldb.SBError()
expected_cfas.append(
process.ReadPointerFromMemory(expected_cfas[-1], error)
)
self.assertSuccess(error, "Managed to read cfa memory")
expected_cfas.append(self.read_ptr_from_memory(process, expected_cfas[-1]))

self.assertEqual(async_cfas, expected_cfas[:-1])

def check_pcs(self, async_frames, process, target):
for idx, frame in enumerate(async_frames[:-1]):
# Read the continuation pointer from the second field of the CFA.
error = lldb.SBError()
continuation_ptr = process.ReadPointerFromMemory(
frame.GetCFA() + target.addr_size, error
continuation_ptr = self.read_ptr_from_memory(
process, frame.GetCFA() + target.addr_size
)
self.assertSuccess(error, "Managed to read context memory")

# The PC of the previous frame should be the continuation pointer
# with the funclet's prologue skipped.
parent_frame = async_frames[idx + 1]
prologue_to_skip = parent_frame.GetFunction().GetPrologueByteSize()
self.assertEqual(continuation_ptr + prologue_to_skip, parent_frame.GetPC())

def check_async_regs_one_frame(self, frame, process):
async_reg_name = "r14" if self.getArchitecture() == "x86_64" else "x22"

cfa = frame.GetCFA()
is_indirect = "await resume" in frame.GetFunctionName()
async_register = frame.FindRegister(async_reg_name).GetValueAsUnsigned()

if is_indirect:
deref_async_reg = self.read_ptr_from_memory(process, async_register)
self.assertEqual(deref_async_reg, cfa)
else:
self.assertEqual(async_register, cfa)

def check_async_regs(self, async_frames, process):
for frame in async_frames:
# The frames from the implicit main function don't have a demangled
# name, so we can't test whether they are a Q funclet or not.
if "Main" in frame.GetFunctionName():
break
self.check_async_regs_one_frame(frame, process)

def check_variables(self, async_frames, expected_values):
for (frame, expected_value) in zip(async_frames, expected_values):
for frame, expected_value in zip(async_frames, expected_values):
myvar = frame.FindVariable("myvar")
lldbutil.check_variable(self, myvar, False, value=expected_value)

Expand All @@ -58,6 +79,7 @@ def test(self):
self.check_cfas(async_frames, process)
self.check_pcs(async_frames, process, target)
self.check_variables(async_frames, ["222", "333", "444", "555"])
self.check_async_regs(async_frames, process)

target.DeleteAllBreakpoints()
target.BreakpointCreateBySourceRegex("breakpoint2", source_file)
Expand All @@ -68,6 +90,7 @@ def test(self):
self.check_cfas(async_frames, process)
self.check_pcs(async_frames, process, target)
self.check_variables(async_frames, ["111", "222", "333", "444", "555"])
self.check_async_regs(async_frames, process)

# Now stop at the Q funclet right after the await to ASYNC___1
target.DeleteAllBreakpoints()
Expand All @@ -77,6 +100,7 @@ def test(self):
self.check_cfas(async_frames, process)
self.check_pcs(async_frames, process, target)
self.check_variables(async_frames, ["222", "333", "444", "555"])
self.check_async_regs(async_frames, process)

target.DeleteAllBreakpoints()
target.BreakpointCreateBySourceRegex("breakpoint3", source_file)
Expand All @@ -85,3 +109,4 @@ def test(self):
self.check_cfas(async_frames, process)
self.check_pcs(async_frames, process, target)
self.check_variables(async_frames, ["222", "333", "444", "555"])
self.check_async_regs(async_frames, process)