[Lldb-commits] [lldb] [lldb] Provide lr value in faulting frame on arm64 (PR #138805)
Jason Molenda via lldb-commits
lldb-commits at lists.llvm.org
Thu May 8 18:47:00 PDT 2025
https://github.com/jasonmolenda updated https://github.com/llvm/llvm-project/pull/138805
>From 4fc9acd03a58a3617b113352c48e5dd2fdb58eda Mon Sep 17 00:00:00 2001
From: Jason Molenda <jmolenda at apple.com>
Date: Tue, 6 May 2025 22:37:17 -0700
Subject: [PATCH 1/6] [lldb] Provide lr value in faulting frame on arm64
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
---
lldb/source/Target/RegisterContextUnwind.cpp | 19 +++++++++++++++----
1 file changed, 15 insertions(+), 4 deletions(-)
diff --git a/lldb/source/Target/RegisterContextUnwind.cpp b/lldb/source/Target/RegisterContextUnwind.cpp
index 3ed49e12476dd..23a86bee2518b 100644
--- a/lldb/source/Target/RegisterContextUnwind.cpp
+++ b/lldb/source/Target/RegisterContextUnwind.cpp
@@ -1377,6 +1377,7 @@ RegisterContextUnwind::SavedLocationForRegister(
}
}
+ // Check if the active_row has a register location listed.
if (regnum.IsValid() &&
active_row->GetRegisterInfo(regnum.GetAsKind(unwindplan_registerkind),
unwindplan_regloc)) {
@@ -1390,11 +1391,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;
@@ -1408,6 +1408,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;
}
}
>From b10162deb49ecddca6439665c2b8ea1995fdd81f Mon Sep 17 00:00:00 2001
From: Jason Molenda <jmolenda at apple.com>
Date: Wed, 7 May 2025 23:24:17 -0700
Subject: [PATCH 2/6] Add the sources for an API test case to be written
---
.../interrupt-and-trap-funcs.s | 92 +++++++++++++++++++
.../macosx/unwind-frameless-faulted/main.c | 6 ++
2 files changed, 98 insertions(+)
create mode 100644 lldb/test/API/macosx/unwind-frameless-faulted/interrupt-and-trap-funcs.s
create mode 100644 lldb/test/API/macosx/unwind-frameless-faulted/main.c
diff --git a/lldb/test/API/macosx/unwind-frameless-faulted/interrupt-and-trap-funcs.s b/lldb/test/API/macosx/unwind-frameless-faulted/interrupt-and-trap-funcs.s
new file mode 100644
index 0000000000000..23eb35c2b31ca
--- /dev/null
+++ b/lldb/test/API/macosx/unwind-frameless-faulted/interrupt-and-trap-funcs.s
@@ -0,0 +1,92 @@
+#define DW_CFA_register 0x9
+#define ehframe_x22 22
+#define ehframe_x23 23
+#define ehframe_pc 32
+
+ .section __TEXT,__text,regular,pure_instructions
+
+//--------------------------------------
+// to_be_interrupted() a frameless function that does a non-ABI
+// function call ("is interrupted/traps" simulated) to trap().
+// Before it branches to trap(), it puts its return address in
+// x23. trap() knows to branch back to $x23 when it has finished.
+//--------------------------------------
+ .globl _to_be_interrupted
+ .p2align 2
+_to_be_interrupted:
+ .cfi_startproc
+
+ // This is a garbage entry to ensure that eh_frame is emitted,
+ // it isn't used for anything. If there's no eh_frame, lldb
+ // can do an assembly emulation scan and add a rule for $lr
+ // which won't expose the issue at hand.
+ .cfi_escape DW_CFA_register, ehframe_x22, ehframe_x23
+ mov x24, x0
+ add x24, x24, #1
+
+ adrp x23, L_.return at PAGE ; put return address in x4
+ add x23, x23, L_.return at PAGEOFF
+
+ b _trap ; branch to trap handler, fake async interrupt
+
+L_.return:
+ 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
+ .p2align 2
+_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
+
+ // standard prologue save of fp & lr so we can call puts()
+ 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]
+ add sp, sp, #32
+
+ // 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
+ .p2align 2
+_break_to_debugger:
+ .cfi_startproc
+
+ // standard prologue save of fp & lr so we can call puts()
+ 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
+
+ brk #0xf000 ;; __builtin_debugtrap aarch64 instruction
+
+ ldp x29, x30, [sp, #16]
+ add sp, sp, #32
+ ret
+ .cfi_endproc
diff --git a/lldb/test/API/macosx/unwind-frameless-faulted/main.c b/lldb/test/API/macosx/unwind-frameless-faulted/main.c
new file mode 100644
index 0000000000000..e121809f25478
--- /dev/null
+++ b/lldb/test/API/macosx/unwind-frameless-faulted/main.c
@@ -0,0 +1,6 @@
+int to_be_interrupted(int);
+int main() {
+ int c = 10;
+ c = to_be_interrupted(c);
+ return c;
+}
>From 508b9cb404072263a6ed52a75b3b7aad5d949510 Mon Sep 17 00:00:00 2001
From: Jason Molenda <jmolenda at apple.com>
Date: Wed, 7 May 2025 23:24:58 -0700
Subject: [PATCH 3/6] Add a log msg when a frame is marked as a trap handler
via the UnwindPlan. And use the trap handler flag for zeroth frame so we can
properly unwind a frameless function that was interrupted while in the trap
handler function.
---
lldb/source/Target/RegisterContextUnwind.cpp | 2 ++
1 file changed, 2 insertions(+)
diff --git a/lldb/source/Target/RegisterContextUnwind.cpp b/lldb/source/Target/RegisterContextUnwind.cpp
index 51c0f56e9bc1e..cf4b96c6eda9f 100644
--- a/lldb/source/Target/RegisterContextUnwind.cpp
+++ b/lldb/source/Target/RegisterContextUnwind.cpp
@@ -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,
@@ -1933,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
>From d8843fe86c0eaaa1483bed0317f832b7630d8548 Mon Sep 17 00:00:00 2001
From: Jason Molenda <jmolenda at apple.com>
Date: Wed, 7 May 2025 23:28:40 -0700
Subject: [PATCH 4/6] Update assembly a bit.
---
.../interrupt-and-trap-funcs.s | 13 ++-----------
1 file changed, 2 insertions(+), 11 deletions(-)
diff --git a/lldb/test/API/macosx/unwind-frameless-faulted/interrupt-and-trap-funcs.s b/lldb/test/API/macosx/unwind-frameless-faulted/interrupt-and-trap-funcs.s
index 23eb35c2b31ca..708ad9ed56a1c 100644
--- a/lldb/test/API/macosx/unwind-frameless-faulted/interrupt-and-trap-funcs.s
+++ b/lldb/test/API/macosx/unwind-frameless-faulted/interrupt-and-trap-funcs.s
@@ -51,7 +51,8 @@ _trap:
// The pc value when we were interrupted is in x23
.cfi_escape DW_CFA_register, ehframe_pc, ehframe_x23
- // standard prologue save of fp & lr so we can call puts()
+ // 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
@@ -76,17 +77,7 @@ _trap:
_break_to_debugger:
.cfi_startproc
- // standard prologue save of fp & lr so we can call puts()
- 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
-
brk #0xf000 ;; __builtin_debugtrap aarch64 instruction
- ldp x29, x30, [sp, #16]
- add sp, sp, #32
ret
.cfi_endproc
>From 1d90aa48c92d9f8ef43c889223c0804ac1491d53 Mon Sep 17 00:00:00 2001
From: Jason Molenda <jmolenda at apple.com>
Date: Thu, 8 May 2025 18:18:14 -0700
Subject: [PATCH 5/6] Add unwind rules for x0 (volatile) and x20 (non-voltile)
to trap() and break_to_debugger() for fun, and add unwind instructions for
the epilogue of trap().
When stopped in `break_to_debugger`,
```
* frame #0: 0x00000001000003e8 a.out`break_to_debugger + 4
frame #1: 0x00000001000003d8 a.out`trap + 16
frame #2: 0x00000001000003c0 a.out`to_be_interrupted + 20
frame #3: 0x0000000100000398 a.out`main + 32
```
Normally we can't provide a volatile register (e.g. x0) up
the stack. And we can provide a non-volatile register (e.g. x20)
up the stack. I added an IsSame rule for trap() and break_to_debugger()
for x0, so it CAN be passed up the stack. And I added an Undefined
rule for x20 to trap() so it CAN'T be provided up the stack.
If a user selects `to_be_interrupted` and does `register read`,
they'll get x0 and they won't get x20. From `main`, they will not
get x0 or x20 (x0 because `to_be_interrupted` doesn't have an IsSame
rule).
---
.../interrupt-and-trap-funcs.s | 25 ++++++++++++++++---
1 file changed, 22 insertions(+), 3 deletions(-)
diff --git a/lldb/test/API/macosx/unwind-frameless-faulted/interrupt-and-trap-funcs.s b/lldb/test/API/macosx/unwind-frameless-faulted/interrupt-and-trap-funcs.s
index 708ad9ed56a1c..b50c4734f18f3 100644
--- a/lldb/test/API/macosx/unwind-frameless-faulted/interrupt-and-trap-funcs.s
+++ b/lldb/test/API/macosx/unwind-frameless-faulted/interrupt-and-trap-funcs.s
@@ -1,4 +1,6 @@
#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
@@ -7,9 +9,9 @@
//--------------------------------------
// to_be_interrupted() a frameless function that does a non-ABI
-// function call ("is interrupted/traps" simulated) to trap().
-// Before it branches to trap(), it puts its return address in
-// x23. trap() knows to branch back to $x23 when it has finished.
+// 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
.p2align 2
@@ -51,6 +53,15 @@ _trap:
// 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's be fun to see lldb not passing a value past this.
+ .cfi_undefined ehframe_x20
+
// standard prologue save of fp & lr so we can call
// break_to_debugger()
sub sp, sp, #32
@@ -63,7 +74,11 @@ _trap:
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
// jump back to $x23 to resume execution of to_be_interrupted
br x23
@@ -77,6 +92,10 @@ _trap:
_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
>From 303edd47f7bcfb6af0a9947ca7b0b9a0c5bec589 Mon Sep 17 00:00:00 2001
From: Jason Molenda <jmolenda at apple.com>
Date: Thu, 8 May 2025 18:46:14 -0700
Subject: [PATCH 6/6] small comment improvement
---
.../unwind-frameless-faulted/interrupt-and-trap-funcs.s | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/lldb/test/API/macosx/unwind-frameless-faulted/interrupt-and-trap-funcs.s b/lldb/test/API/macosx/unwind-frameless-faulted/interrupt-and-trap-funcs.s
index b50c4734f18f3..38be06a681bf2 100644
--- a/lldb/test/API/macosx/unwind-frameless-faulted/interrupt-and-trap-funcs.s
+++ b/lldb/test/API/macosx/unwind-frameless-faulted/interrupt-and-trap-funcs.s
@@ -18,10 +18,10 @@
_to_be_interrupted:
.cfi_startproc
- // This is a garbage entry to ensure that eh_frame is emitted,
- // it isn't used for anything. If there's no eh_frame, lldb
- // can do an assembly emulation scan and add a rule for $lr
- // which won't expose the issue at hand.
+ // 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
More information about the lldb-commits
mailing list