[lldb] Provide lr value in faulting frame on arm64 (#138805)

Re-landing this patch with small tweaks to address CI bot failures
as it was run on many different configurations.  I think the test
may run on aarch64 Linux systems now.

When a frameless function faults or is interrupted asynchronously, the
UnwindPlan MAY have no register location rule for the return address
register (lr on arm64); the value is simply live in the lr register when
it was interrupted, and the frame below this on the stack -- e.g.
sigtramp on a Unix system -- has the full register context, including
that register.

RegisterContextUnwind::SavedLocationForRegister, when asked to find the
caller's pc value, will first see if there is a pc register location. If
there isn't, on a Return Address Register architecture like
arm/mips/riscv, we rewrite the register request from "pc" to "RA
register", and search for a location.

On frame 0 (the live frame) and an interrupted frame, the UnwindPlan may
have no register location rule for the RA Reg, that is valid. A
frameless function that never calls another may simply keep the return
address in the live register the whole way. Our instruction emulation
unwind plans explicitly add a rule (see Pavel's May 2024 change
https://github.com/llvm/llvm-project/pull/91321 ), but an UnwindPlan
sourced from debug_frame may not.

I've got a case where this exactly happens - clang debug_frame for arm64
where there is no register location for the lr in a frameless function.
There is a fault in the middle of this frameless function and we only
get the lr value from the fault handler below this frame if lr has a
register location of `IsSame`, in line with Pavel's 2024 change.

Similar to how we see a request of the RA Reg from frame 0 after failing
to find an unwind location for the pc register, the same style of
special casing is needed when this is a function that was interrupted.

Without this change, we can find the pc of the frame that was executing
when it was interrupted, but we need $lr to find its caller, and we
don't descend down to the trap handler to get that value, truncating the
stack.

rdar://145614545
This commit is contained in:
Jason Molenda
2025-05-11 23:38:11 -07:00
parent 87b4cacf2d
commit b957cc0c7e
5 changed files with 306 additions and 10 deletions

View File

@@ -248,6 +248,7 @@ void RegisterContextUnwind::InitializeZerothFrame() {
active_row =
m_full_unwind_plan_sp->GetRowForFunctionOffset(m_current_offset);
row_register_kind = m_full_unwind_plan_sp->GetRegisterKind();
PropagateTrapHandlerFlagFromUnwindPlan(m_full_unwind_plan_sp);
if (active_row && log) {
StreamString active_row_strm;
active_row->Dump(active_row_strm, m_full_unwind_plan_sp.get(), &m_thread,
@@ -279,7 +280,7 @@ void RegisterContextUnwind::InitializeZerothFrame() {
call_site_unwind_plan = func_unwinders_sp->GetUnwindPlanAtCallSite(
process->GetTarget(), m_thread);
if (call_site_unwind_plan != nullptr) {
if (call_site_unwind_plan.get() != nullptr) {
m_fallback_unwind_plan_sp = call_site_unwind_plan;
if (TryFallbackUnwindPlan())
cfa_status = true;
@@ -1375,6 +1376,7 @@ RegisterContextUnwind::SavedLocationForRegister(
}
}
// Check if the active_row has a register location listed.
if (regnum.IsValid() && active_row &&
active_row->GetRegisterInfo(regnum.GetAsKind(unwindplan_registerkind),
unwindplan_regloc)) {
@@ -1388,11 +1390,10 @@ RegisterContextUnwind::SavedLocationForRegister(
// This is frame 0 and we're retrieving the PC and it's saved in a Return
// Address register and it hasn't been saved anywhere yet -- that is,
// it's still live in the actual register. Handle this specially.
if (!have_unwindplan_regloc && return_address_reg.IsValid() &&
IsFrameZero()) {
if (return_address_reg.GetAsKind(eRegisterKindLLDB) !=
LLDB_INVALID_REGNUM) {
return_address_reg.GetAsKind(eRegisterKindLLDB) !=
LLDB_INVALID_REGNUM) {
if (IsFrameZero()) {
lldb_private::UnwindLLDB::ConcreteRegisterLocation new_regloc;
new_regloc.type = UnwindLLDB::ConcreteRegisterLocation::
eRegisterInLiveRegisterContext;
@@ -1406,6 +1407,17 @@ RegisterContextUnwind::SavedLocationForRegister(
return_address_reg.GetAsKind(eRegisterKindLLDB),
return_address_reg.GetAsKind(eRegisterKindLLDB));
return UnwindLLDB::RegisterSearchResult::eRegisterFound;
} else if (BehavesLikeZerothFrame()) {
// This function was interrupted asynchronously -- it faulted,
// an async interrupt, a timer fired, a debugger expression etc.
// The caller's pc is in the Return Address register, but the
// UnwindPlan for this function may have no location rule for
// the RA reg.
// This means that the caller's return address is in the RA reg
// when the function was interrupted--descend down one stack frame
// to retrieve it from the trap handler's saved context.
unwindplan_regloc.SetSame();
have_unwindplan_regloc = true;
}
}
@@ -1722,10 +1734,10 @@ RegisterContextUnwind::SavedLocationForRegister(
// tricky frame and our usual techniques can continue to be used.
bool RegisterContextUnwind::TryFallbackUnwindPlan() {
if (m_fallback_unwind_plan_sp == nullptr)
if (m_fallback_unwind_plan_sp.get() == nullptr)
return false;
if (m_full_unwind_plan_sp == nullptr)
if (m_full_unwind_plan_sp.get() == nullptr)
return false;
if (m_full_unwind_plan_sp.get() == m_fallback_unwind_plan_sp.get() ||
@@ -1773,7 +1785,7 @@ bool RegisterContextUnwind::TryFallbackUnwindPlan() {
// fallback UnwindPlan. We checked if m_fallback_unwind_plan_sp was nullptr
// at the top -- the only way it became nullptr since then is via
// SavedLocationForRegister().
if (m_fallback_unwind_plan_sp == nullptr)
if (m_fallback_unwind_plan_sp.get() == nullptr)
return true;
// Switch the full UnwindPlan to be the fallback UnwindPlan. If we decide
@@ -1862,10 +1874,10 @@ bool RegisterContextUnwind::TryFallbackUnwindPlan() {
}
bool RegisterContextUnwind::ForceSwitchToFallbackUnwindPlan() {
if (m_fallback_unwind_plan_sp == nullptr)
if (m_fallback_unwind_plan_sp.get() == nullptr)
return false;
if (m_full_unwind_plan_sp == nullptr)
if (m_full_unwind_plan_sp.get() == nullptr)
return false;
if (m_full_unwind_plan_sp.get() == m_fallback_unwind_plan_sp.get() ||
@@ -1922,6 +1934,7 @@ void RegisterContextUnwind::PropagateTrapHandlerFlagFromUnwindPlan(
}
m_frame_type = eTrapHandlerFrame;
UnwindLogMsg("This frame is marked as a trap handler via its UnwindPlan");
if (m_current_offset_backed_up_one != m_current_offset) {
// We backed up the pc by 1 to compute the symbol context, but

View File

@@ -0,0 +1,13 @@
C_SOURCES := main.c
interrupt-and-trap-funcs.o: interrupt-and-trap-funcs.s
$(CC) $(CFLAGS) -E -o interrupt-and-trap-funcs.s $(SRCDIR)/interrupt-and-trap-funcs.s
$(CC) $(CFLAGS) -c -o interrupt-and-trap-funcs.o interrupt-and-trap-funcs.s
include Makefile.rules
a.out: interrupt-and-trap-funcs.o
# Needs to come after include
OBJECTS += interrupt-and-trap-funcs.o

View File

@@ -0,0 +1,128 @@
"""Test that lldb backtraces a frameless function that faults correctly."""
import lldbsuite.test.lldbutil as lldbutil
from lldbsuite.test.lldbtest import *
from lldbsuite.test.decorators import *
import shutil
import os
class TestUnwindFramelessFaulted(TestBase):
NO_DEBUG_INFO_TESTCASE = True
@skipIf(oslist=no_match([lldbplatformutil.getDarwinOSTriples(), "linux"]))
@skipIf(archs=no_match(["aarch64", "arm64", "arm64e"]))
# The static linker in Xcode 15.0-15.2 on macOS 14 will mislink
# the eh_frame addresses; ld-classic in those tools is one workaround.
# This issue was fixed in Xcode 15.3, but it's not straightforward
# to test for the linker version or Xcode version so tie this to
# macOS 15 which uses Xcode 16 and does not have the issues.
@skipIf(macos_version=["<", "15.0"])
def test_frameless_faulted_unwind(self):
self.build()
(target, process, thread, bp) = lldbutil.run_to_name_breakpoint(
self, "main", only_one_thread=False
)
# The test program will have a backtrace like this at its deepest:
#
# * frame #0: 0x0000000102adc468 a.out`break_to_debugger + 4
# frame #1: 0x0000000102adc458 a.out`trap + 16
# frame #2: 0x0000000102adc440 a.out`to_be_interrupted + 20
# frame #3: 0x0000000102adc418 a.out`main at main.c:4:7
# frame #4: 0x0000000193b7eb4c dyld`start + 6000
correct_frames = ["break_to_debugger", "trap", "to_be_interrupted", "main"]
# Keep track of when main has branch & linked, instruction step until we're
# back in main()
main_has_bl_ed = False
# Instruction step through the binary until we are in a function not
# listed in correct_frames.
frame = thread.GetFrameAtIndex(0)
step_count = 0
max_step_count = 200
while (
process.GetState() == lldb.eStateStopped
and frame.name in correct_frames
and step_count < max_step_count
):
starting_index = 0
if self.TraceOn():
self.runCmd("bt")
# Find which index into correct_frames the current stack frame is
for idx, name in enumerate(correct_frames):
if frame.name == name:
starting_index = idx
# Test that all frames after the current frame listed in
# correct_frames appears in the backtrace.
frame_idx = 0
for expected_frame in correct_frames[starting_index:]:
self.assertEqual(thread.GetFrameAtIndex(frame_idx).name, expected_frame)
frame_idx = frame_idx + 1
# When we're at our deepest level, test that register passing of
# x0 and x20 follow the by-hand UnwindPlan rules.
# In this test program, we can get x0 in the middle of the stack
# and we CAN'T get x20. The opposites of the normal AArch64 SysV
# ABI.
if frame.name == "break_to_debugger":
tbi_frame = thread.GetFrameAtIndex(2)
self.assertEqual(tbi_frame.name, "to_be_interrupted")
# The original argument to to_be_interrupted(), 10
# Normally can't get x0 mid-stack, but UnwindPlans have
# special rules to make this possible.
x0_reg = tbi_frame.register["x0"]
self.assertTrue(x0_reg.IsValid())
self.assertEqual(x0_reg.GetValueAsUnsigned(), 10)
# The incremented return value from to_be_interrupted(), 11
x24_reg = tbi_frame.register["x24"]
self.assertTrue(x24_reg.IsValid())
self.assertEqual(x24_reg.GetValueAsUnsigned(), 11)
# x20 can normally be fetched mid-stack, but the UnwindPlan
# has a rule saying it can't be fetched.
x20_reg = tbi_frame.register["x20"]
self.assertTrue(x20_reg.error.fail)
trap_frame = thread.GetFrameAtIndex(1)
self.assertEqual(trap_frame.name, "trap")
# Confirm that we can fetch x0 from trap() which
# is normally not possible w/ SysV AbI, but special
# UnwindPlans in use.
x0_reg = trap_frame.register["x0"]
self.assertTrue(x0_reg.IsValid())
self.assertEqual(x0_reg.GetValueAsUnsigned(), 10)
x1_reg = trap_frame.register["x1"]
self.assertTrue(x1_reg.error.fail)
main_frame = thread.GetFrameAtIndex(3)
self.assertEqual(main_frame.name, "main")
# x20 can normally be fetched mid-stack, but the UnwindPlan
# has a rule saying it can't be fetched.
x20_reg = main_frame.register["x20"]
self.assertTrue(x20_reg.error.fail)
# x21 can be fetched mid-stack.
x21_reg = main_frame.register["x21"]
self.assertTrue(x21_reg.error.success)
# manually move past the BRK instruction in
# break_to_debugger(). lldb-server doesn't
# advance past the builtin_debugtrap() BRK
# instruction.
if (
thread.GetStopReason() == lldb.eStopReasonException
and frame.name == "break_to_debugger"
):
frame.SetPC(frame.GetPC() + 4)
if self.TraceOn():
print("StepInstruction")
thread.StepInstruction(False)
frame = thread.GetFrameAtIndex(0)
step_count = step_count + 1

View File

@@ -0,0 +1,135 @@
// This is assembly code that needs to be run
// through the preprocessor, for simplicity of
// preprocessing it's named .c to start with.
//
// clang-format off
#define DW_CFA_register 0x9
#define ehframe_x0 0
#define ehframe_x20 20
#define ehframe_x22 22
#define ehframe_x23 23
#define ehframe_pc 32
#if defined(__APPLE__)
#define TO_BE_INTERRUPTED _to_be_interrupted
#define TRAP _trap
#define BREAK_TO_DEBUGGER _break_to_debugger
#else
#define TO_BE_INTERRUPTED to_be_interrupted
#define TRAP trap
#define BREAK_TO_DEBUGGER break_to_debugger
#endif
.text
//--------------------------------------
// to_be_interrupted() a frameless function that does a non-ABI
// function call to trap(), simulating an async signal/interrup/exception/fault.
// Before it branches to trap(), put the return address in x23.
// trap() knows to branch back to $x23 when it has finished.
//--------------------------------------
.globl TO_BE_INTERRUPTED
#if defined(__APPLE__)
.p2align 2
#endif
TO_BE_INTERRUPTED:
.cfi_startproc
// This is a garbage entry to ensure that eh_frame is emitted.
// If there's no eh_frame, lldb can use the assembly emulation scan,
// which always includes a rule for $lr, and we won't replicate the
// bug we're testing for.
.cfi_escape DW_CFA_register, ehframe_x22, ehframe_x23
mov x24, x0
add x24, x24, #1
#if defined(__APPLE__)
adrp x23, L_.return@PAGE // put return address in x23
add x23, x23, L_.return@PAGEOFF
#else
adrp x23, .L.return
add x23, x23, :lo12:.L.return
#endif
b TRAP // branch to trap handler, fake async interrupt
#if defined(__APPLE__)
L_.return:
#else
.L.return:
#endif
mov x0, x24
ret
.cfi_endproc
//--------------------------------------
// trap() trap handler function, sets up stack frame
// with special unwind rule for the pc value of the
// "interrupted" stack frame (it's in x23), then calls
// break_to_debugger().
//--------------------------------------
.globl TRAP
#if defined(__APPLE__)
.p2align 2
#endif
TRAP:
.cfi_startproc
.cfi_signal_frame
// The pc value when we were interrupted is in x23
.cfi_escape DW_CFA_register, ehframe_pc, ehframe_x23
// For fun, mark x0 as unmodified so the caller can
// retrieve the value if it wants.
.cfi_same_value ehframe_x0
// Mark x20 as undefined. This is a callee-preserved
// (non-volatile) register by the SysV AArch64 ABI, but
// it'll be fun to see lldb not passing a value past this
// point on the stack.
.cfi_undefined ehframe_x20
// standard prologue save of fp & lr so we can call
// break_to_debugger()
sub sp, sp, #32
stp x29, x30, [sp, #16]
add x29, sp, #16
.cfi_def_cfa w29, 16
.cfi_offset w30, -8
.cfi_offset w29, -16
bl BREAK_TO_DEBUGGER
ldp x29, x30, [sp, #16]
.cfi_same_value x29
.cfi_same_value x30
.cfi_def_cfa sp, 32
add sp, sp, #32
.cfi_same_value sp
.cfi_def_cfa sp, 0
// jump back to $x23 to resume execution of to_be_interrupted
br x23
.cfi_endproc
//--------------------------------------
// break_to_debugger() executes a BRK instruction
//--------------------------------------
.globl BREAK_TO_DEBUGGER
#if defined(__APPLE__)
.p2align 2
#endif
BREAK_TO_DEBUGGER:
.cfi_startproc
// For fun, mark x0 as unmodified so the caller can
// retrieve the value if it wants.
.cfi_same_value ehframe_x0
brk #0xf000 // __builtin_debugtrap aarch64 instruction
ret
.cfi_endproc

View File

@@ -0,0 +1,7 @@
int to_be_interrupted(int);
int main() {
int c = 10;
c = to_be_interrupted(c);
return c;
}