mirror of
https://github.com/intel/llvm.git
synced 2026-01-16 05:32:28 +08:00
[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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
int to_be_interrupted(int);
|
||||
|
||||
int main() {
|
||||
int c = 10;
|
||||
c = to_be_interrupted(c);
|
||||
return c;
|
||||
}
|
||||
Reference in New Issue
Block a user