Skip to content

Commit e897cb1

Browse files
authored
[lldb] Provide lr value in faulting frame on arm64 (#138805)
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 #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
1 parent 2d2d753 commit e897cb1

File tree

5 files changed

+285
-4
lines changed

5 files changed

+285
-4
lines changed

lldb/source/Target/RegisterContextUnwind.cpp

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ void RegisterContextUnwind::InitializeZerothFrame() {
248248
active_row =
249249
m_full_unwind_plan_sp->GetRowForFunctionOffset(m_current_offset);
250250
row_register_kind = m_full_unwind_plan_sp->GetRegisterKind();
251+
PropagateTrapHandlerFlagFromUnwindPlan(m_full_unwind_plan_sp);
251252
if (active_row && log) {
252253
StreamString active_row_strm;
253254
active_row->Dump(active_row_strm, m_full_unwind_plan_sp.get(), &m_thread,
@@ -1375,6 +1376,7 @@ RegisterContextUnwind::SavedLocationForRegister(
13751376
}
13761377
}
13771378

1379+
// Check if the active_row has a register location listed.
13781380
if (regnum.IsValid() && active_row &&
13791381
active_row->GetRegisterInfo(regnum.GetAsKind(unwindplan_registerkind),
13801382
unwindplan_regloc)) {
@@ -1388,11 +1390,10 @@ RegisterContextUnwind::SavedLocationForRegister(
13881390
// This is frame 0 and we're retrieving the PC and it's saved in a Return
13891391
// Address register and it hasn't been saved anywhere yet -- that is,
13901392
// it's still live in the actual register. Handle this specially.
1391-
13921393
if (!have_unwindplan_regloc && return_address_reg.IsValid() &&
1393-
IsFrameZero()) {
1394-
if (return_address_reg.GetAsKind(eRegisterKindLLDB) !=
1395-
LLDB_INVALID_REGNUM) {
1394+
return_address_reg.GetAsKind(eRegisterKindLLDB) !=
1395+
LLDB_INVALID_REGNUM) {
1396+
if (IsFrameZero()) {
13961397
lldb_private::UnwindLLDB::ConcreteRegisterLocation new_regloc;
13971398
new_regloc.type = UnwindLLDB::ConcreteRegisterLocation::
13981399
eRegisterInLiveRegisterContext;
@@ -1406,6 +1407,17 @@ RegisterContextUnwind::SavedLocationForRegister(
14061407
return_address_reg.GetAsKind(eRegisterKindLLDB),
14071408
return_address_reg.GetAsKind(eRegisterKindLLDB));
14081409
return UnwindLLDB::RegisterSearchResult::eRegisterFound;
1410+
} else if (BehavesLikeZerothFrame()) {
1411+
// This function was interrupted asynchronously -- it faulted,
1412+
// an async interrupt, a timer fired, a debugger expression etc.
1413+
// The caller's pc is in the Return Address register, but the
1414+
// UnwindPlan for this function may have no location rule for
1415+
// the RA reg.
1416+
// This means that the caller's return address is in the RA reg
1417+
// when the function was interrupted--descend down one stack frame
1418+
// to retrieve it from the trap handler's saved context.
1419+
unwindplan_regloc.SetSame();
1420+
have_unwindplan_regloc = true;
14091421
}
14101422
}
14111423

@@ -1922,6 +1934,7 @@ void RegisterContextUnwind::PropagateTrapHandlerFlagFromUnwindPlan(
19221934
}
19231935

19241936
m_frame_type = eTrapHandlerFrame;
1937+
UnwindLogMsg("This frame is marked as a trap handler via its UnwindPlan");
19251938

19261939
if (m_current_offset_backed_up_one != m_current_offset) {
19271940
// We backed up the pc by 1 to compute the symbol context, but
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
C_SOURCES := main.c
2+
3+
interrupt-and-trap-funcs.o: interrupt-and-trap-funcs.c
4+
$(CC) $(CFLAGS) -E -o interrupt-and-trap-funcs.s $(SRCDIR)/interrupt-and-trap-funcs.c
5+
$(CC) $(CFLAGS) -c -o interrupt-and-trap-funcs.o interrupt-and-trap-funcs.s
6+
7+
include Makefile.rules
8+
9+
a.out: interrupt-and-trap-funcs.o
10+
11+
# Needs to come after include
12+
OBJECTS += interrupt-and-trap-funcs.o
13+
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"""Test that lldb backtraces a frameless function that faults correctly."""
2+
3+
import lldbsuite.test.lldbutil as lldbutil
4+
from lldbsuite.test.lldbtest import *
5+
from lldbsuite.test.decorators import *
6+
import shutil
7+
import os
8+
9+
10+
class TestUnwindFramelessFaulted(TestBase):
11+
NO_DEBUG_INFO_TESTCASE = True
12+
13+
@skipIf(
14+
oslist=no_match([lldbplatformutil.getDarwinOSTriples(), "linux"]),
15+
archs=no_match(["aarch64", "arm64", "arm64e"]),
16+
)
17+
def test_frameless_faulted_unwind(self):
18+
self.build()
19+
20+
(target, process, thread, bp) = lldbutil.run_to_name_breakpoint(
21+
self, "main", only_one_thread=False
22+
)
23+
24+
# The test program will have a backtrace like this at its deepest:
25+
#
26+
# * frame #0: 0x0000000102adc468 a.out`break_to_debugger + 4
27+
# frame #1: 0x0000000102adc458 a.out`trap + 16
28+
# frame #2: 0x0000000102adc440 a.out`to_be_interrupted + 20
29+
# frame #3: 0x0000000102adc418 a.out`main at main.c:4:7
30+
# frame #4: 0x0000000193b7eb4c dyld`start + 6000
31+
32+
correct_frames = ["break_to_debugger", "trap", "to_be_interrupted", "main"]
33+
34+
# Keep track of when main has branch & linked, instruction step until we're
35+
# back in main()
36+
main_has_bl_ed = False
37+
38+
# Instruction step through the binary until we are in a function not
39+
# listed in correct_frames.
40+
frame = thread.GetFrameAtIndex(0)
41+
step_count = 0
42+
max_step_count = 200
43+
while (
44+
process.GetState() == lldb.eStateStopped
45+
and frame.name in correct_frames
46+
and step_count < max_step_count
47+
):
48+
starting_index = 0
49+
if self.TraceOn():
50+
self.runCmd("bt")
51+
52+
# Find which index into correct_frames the current stack frame is
53+
for idx, name in enumerate(correct_frames):
54+
if frame.name == name:
55+
starting_index = idx
56+
57+
# Test that all frames after the current frame listed in
58+
# correct_frames appears in the backtrace.
59+
frame_idx = 0
60+
for expected_frame in correct_frames[starting_index:]:
61+
self.assertEqual(thread.GetFrameAtIndex(frame_idx).name, expected_frame)
62+
frame_idx = frame_idx + 1
63+
64+
# When we're at our deepest level, test that register passing of
65+
# x0 and x20 follow the by-hand UnwindPlan rules.
66+
# In this test program, we can get x0 in the middle of the stack
67+
# and we CAN'T get x20. The opposites of the normal AArch64 SysV
68+
# ABI.
69+
if frame.name == "break_to_debugger":
70+
tbi_frame = thread.GetFrameAtIndex(2)
71+
self.assertEqual(tbi_frame.name, "to_be_interrupted")
72+
# The original argument to to_be_interrupted(), 10
73+
# Normally can't get x0 mid-stack, but UnwindPlans have
74+
# special rules to make this possible.
75+
x0_reg = tbi_frame.register["x0"]
76+
self.assertTrue(x0_reg.IsValid())
77+
self.assertEqual(x0_reg.GetValueAsUnsigned(), 10)
78+
# The incremented return value from to_be_interrupted(), 11
79+
x24_reg = tbi_frame.register["x24"]
80+
self.assertTrue(x24_reg.IsValid())
81+
self.assertEqual(x24_reg.GetValueAsUnsigned(), 11)
82+
# x20 can normally be fetched mid-stack, but the UnwindPlan
83+
# has a rule saying it can't be fetched.
84+
x20_reg = tbi_frame.register["x20"]
85+
self.assertTrue(x20_reg.error.fail)
86+
87+
trap_frame = thread.GetFrameAtIndex(1)
88+
self.assertEqual(trap_frame.name, "trap")
89+
# Confirm that we can fetch x0 from trap() which
90+
# is normally not possible w/ SysV AbI, but special
91+
# UnwindPlans in use.
92+
x0_reg = trap_frame.register["x0"]
93+
self.assertTrue(x0_reg.IsValid())
94+
self.assertEqual(x0_reg.GetValueAsUnsigned(), 10)
95+
x1_reg = trap_frame.register["x1"]
96+
self.assertTrue(x1_reg.error.fail)
97+
98+
main_frame = thread.GetFrameAtIndex(3)
99+
self.assertEqual(main_frame.name, "main")
100+
# x20 can normally be fetched mid-stack, but the UnwindPlan
101+
# has a rule saying it can't be fetched.
102+
x20_reg = main_frame.register["x20"]
103+
self.assertTrue(x20_reg.error.fail)
104+
# x21 can be fetched mid-stack.
105+
x21_reg = main_frame.register["x21"]
106+
self.assertTrue(x21_reg.error.success)
107+
108+
# manually move past the BRK instruction in
109+
# break_to_debugger(). lldb-server doesn't
110+
# advance past the builtin_debugtrap() BRK
111+
# instruction.
112+
if (
113+
thread.GetStopReason() == lldb.eStopReasonException
114+
and frame.name == "break_to_debugger"
115+
):
116+
frame.SetPC(frame.GetPC() + 4)
117+
118+
if self.TraceOn():
119+
print("StepInstruction")
120+
thread.StepInstruction(False)
121+
frame = thread.GetFrameAtIndex(0)
122+
step_count = step_count + 1
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// This is assembly code that needs to be run
2+
// through the preprocessor, for simplicity of
3+
// preprocessing it's named .c to start with.
4+
//
5+
// clang-format off
6+
7+
8+
#define DW_CFA_register 0x9
9+
#define ehframe_x0 0
10+
#define ehframe_x20 20
11+
#define ehframe_x22 22
12+
#define ehframe_x23 23
13+
#define ehframe_pc 32
14+
15+
#if defined(__APPLE__)
16+
#define TO_BE_INTERRUPTED _to_be_interrupted
17+
#define TRAP _trap
18+
#define BREAK_TO_DEBUGGER _break_to_debugger
19+
#else
20+
#define TO_BE_INTERRUPTED to_be_interrupted
21+
#define TRAP trap
22+
#define BREAK_TO_DEBUGGER break_to_debugger
23+
#endif
24+
25+
.text
26+
//--------------------------------------
27+
// to_be_interrupted() a frameless function that does a non-ABI
28+
// function call to trap(), simulating an async signal/interrup/exception/fault.
29+
// Before it branches to trap(), put the return address in x23.
30+
// trap() knows to branch back to $x23 when it has finished.
31+
//--------------------------------------
32+
.globl TO_BE_INTERRUPTED
33+
TO_BE_INTERRUPTED:
34+
.cfi_startproc
35+
36+
// This is a garbage entry to ensure that eh_frame is emitted.
37+
// If there's no eh_frame, lldb can use the assembly emulation scan,
38+
// which always includes a rule for $lr, and we won't replicate the
39+
// bug we're testing for.
40+
.cfi_escape DW_CFA_register, ehframe_x22, ehframe_x23
41+
mov x24, x0
42+
add x24, x24, #1
43+
44+
#if defined(__APPLE__)
45+
adrp x23, L_.return@PAGE // put return address in x23
46+
add x23, x23, L_.return@PAGEOFF
47+
#else
48+
adrp x23, .L.return
49+
add x23, x23, :lo12:.L.return
50+
#endif
51+
52+
b TRAP // branch to trap handler, fake async interrupt
53+
54+
#if defined(__APPLE__)
55+
L_.return:
56+
#else
57+
.L.return:
58+
#endif
59+
mov x0, x24
60+
ret
61+
.cfi_endproc
62+
63+
64+
65+
//--------------------------------------
66+
// trap() trap handler function, sets up stack frame
67+
// with special unwind rule for the pc value of the
68+
// "interrupted" stack frame (it's in x23), then calls
69+
// break_to_debugger().
70+
//--------------------------------------
71+
.globl TRAP
72+
TRAP:
73+
.cfi_startproc
74+
.cfi_signal_frame
75+
76+
// The pc value when we were interrupted is in x23
77+
.cfi_escape DW_CFA_register, ehframe_pc, ehframe_x23
78+
79+
// For fun, mark x0 as unmodified so the caller can
80+
// retrieve the value if it wants.
81+
.cfi_same_value ehframe_x0
82+
83+
// Mark x20 as undefined. This is a callee-preserved
84+
// (non-volatile) register by the SysV AArch64 ABI, but
85+
// it'll be fun to see lldb not passing a value past this
86+
// point on the stack.
87+
.cfi_undefined ehframe_x20
88+
89+
// standard prologue save of fp & lr so we can call
90+
// break_to_debugger()
91+
sub sp, sp, #32
92+
stp x29, x30, [sp, #16]
93+
add x29, sp, #16
94+
.cfi_def_cfa w29, 16
95+
.cfi_offset w30, -8
96+
.cfi_offset w29, -16
97+
98+
bl BREAK_TO_DEBUGGER
99+
100+
ldp x29, x30, [sp, #16]
101+
.cfi_same_value x29
102+
.cfi_same_value x30
103+
.cfi_def_cfa sp, 32
104+
add sp, sp, #32
105+
.cfi_same_value sp
106+
.cfi_def_cfa sp, 0
107+
108+
// jump back to $x23 to resume execution of to_be_interrupted
109+
br x23
110+
.cfi_endproc
111+
112+
//--------------------------------------
113+
// break_to_debugger() executes a BRK instruction
114+
//--------------------------------------
115+
.globl BREAK_TO_DEBUGGER
116+
BREAK_TO_DEBUGGER:
117+
.cfi_startproc
118+
119+
// For fun, mark x0 as unmodified so the caller can
120+
// retrieve the value if it wants.
121+
.cfi_same_value ehframe_x0
122+
123+
brk #0xf000 // __builtin_debugtrap aarch64 instruction
124+
125+
ret
126+
.cfi_endproc
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
int to_be_interrupted(int);
2+
3+
int main() {
4+
int c = 10;
5+
c = to_be_interrupted(c);
6+
return c;
7+
}

0 commit comments

Comments
 (0)