[Lldb-commits] [lldb] [lldb] Don't trust unwind information for outlined functions (PR #204500)
Jason Molenda via lldb-commits
lldb-commits at lists.llvm.org
Thu Jun 25 17:47:17 PDT 2026
https://github.com/jasonmolenda updated https://github.com/llvm/llvm-project/pull/204500
>From 6c1d201c00e4320da57ef11dafc65a58a7221bf9 Mon Sep 17 00:00:00 2001
From: Jason Molenda <jmolenda at apple.com>
Date: Wed, 17 Jun 2026 18:59:25 -0700
Subject: [PATCH 1/4] [lldb] Don't trust unwind information for outlined
functions
clang/llvm has a feature to take identical sequences of instructions
from multiple functions, put them in a separate utility function,
and call that utility function, reducing overall codesize. Primarly
useful in memory constrained environments.
The CFI unwind instructions for these OUTLINED_FUNCTIONs in
eh_frame/debug_frame is entirely incorrect; clang does not
emit any directives for them.
Furthermore, the callers sometimes call the OUTLINED_FUNCTION in a
non-ABI call manner. For instance, if the last few instructions
of a function (including the epilogue) are put in an OUTLINED_FUNCTION,
a function will simply tail-call/jump to the outlined function.
If a function prologue is separated into an OUTLINED_FUNCTION, a
non ABI call to preserve the return-address-register may be used,
e.g. on RISVC a `jal t0, OUTLINED_FUNCTION_nn` may be used, putting
the return address in the t0 temporary register; lldb's instruction
analysis while in the OUTLINED_FUNCTION will not understand that
this non-ABI calling method was used.
This PR overrides any normal unwind information when we find a stack
frame in an OUTLINED_FUNCTION and replaces it with the architectural
default unwind plan, hopefully this is a target where stack frames
start with a saved fp+pc up the stack, and there is an arch default
unwind plan that can get past this point while trusting no sources
of unwind information. We may end up skipping a stack frame in the
process (the caller of the OUTLINED_FUNCTION), but it's the best
we can do.
There is a further problem not handled here, where the prologue of
a function is outlined. None of the unwind information for this
function will be correct - not CFI directives in eh_frame/debug_frame
no lldb's instruction emulation. We need to also treat this case
as "use the arch default unwind plan, it's our only hope" but I
need to come up with a way to detect this scenario still.
This test case includes a main.c and outlined-function.s which
simulates what the three different OUTLINED_FUNCTION combinations
look like in AArch64 assembly, but I've never succeeded at
getting assembly to compile cross-platform, so I also created a
corefile and JSON binary (arm64-outlined-epilogue-core.yaml,
binary.json) and run yaml2macho-core to create a Mach-O corefile
to run the test case against. This corefile is simply me
capturing some memory and register contents while debugging the
main.c and outlined-function.s.
rdar://179530769
---
lldb/source/Target/RegisterContextUnwind.cpp | 35 +++++
.../unwind/outlined-functions/Makefile | 1 +
.../TestBacktraceThroughOutlinedFunction.py | 54 ++++++++
.../arm64-outlined-epilogue-core.yaml | 98 ++++++++++++++
.../unwind/outlined-functions/binary.json | 59 +++++++++
.../unwind/outlined-functions/main.c | 25 ++++
.../outlined-functions/outlined-functions.s | 121 ++++++++++++++++++
7 files changed, 393 insertions(+)
create mode 100644 lldb/test/API/functionalities/unwind/outlined-functions/Makefile
create mode 100644 lldb/test/API/functionalities/unwind/outlined-functions/TestBacktraceThroughOutlinedFunction.py
create mode 100644 lldb/test/API/functionalities/unwind/outlined-functions/arm64-outlined-epilogue-core.yaml
create mode 100644 lldb/test/API/functionalities/unwind/outlined-functions/binary.json
create mode 100644 lldb/test/API/functionalities/unwind/outlined-functions/main.c
create mode 100644 lldb/test/API/functionalities/unwind/outlined-functions/outlined-functions.s
diff --git a/lldb/source/Target/RegisterContextUnwind.cpp b/lldb/source/Target/RegisterContextUnwind.cpp
index 0039b29034f4d..4aef810a7d23a 100644
--- a/lldb/source/Target/RegisterContextUnwind.cpp
+++ b/lldb/source/Target/RegisterContextUnwind.cpp
@@ -868,6 +868,41 @@ RegisterContextUnwind::GetFullUnwindPlanForFrame() {
return arch_default_unwind_plan_sp;
}
+ // Function outlining is a clang feature where common blocks of instructions
+ // from separate functions can be put in a separate utility function, and
+ // the original functions call into the utility function to execute them,
+ // resulting in fewer bytes used for the code section overall.
+ //
+ // The call to the OUTLINED_FUNCTION may not be a normal ABI call (e.g.
+ // on RISCV it might be called `jal t0, OUTLINED_FUNCTION_<nn>` putting the
+ // return address in a temporary register instead of $ra). Tthe unwind
+ // instructions in eh_frame/debug_frame are not correct today for an
+ // OUTLINED_FUNCTION, even when a normal ABI call is made.
+ // Clang does not insert CFI directives in the OUTLINED_FUNCTION so it
+ // will look like a frameless function that does nothing beyond its initial
+ // unwind state.
+ //
+ // A function may outline its prologue, mid-function block of instructions,
+ // or its epilogue. If a function's prologue is outlined, the unwind
+ // instructions (eh_frame/debug_frame/lldb's instruction analysis) for the
+ // original function are incorrect and cannot be trusted.
+ //
+ // If we are in an OUTLINED_FUNCTION in a backtrace, distrust all sources
+ // of unwind information and use the architectural default unwind plan
+ // instead. We may miss the caller stack frame in the backtrace, but it
+ // will work more reliably than any other method.
+ if (m_sym_ctx_valid && arch_default_unwind_plan_sp) {
+ const char *name = GetSymbolOrFunctionName(m_sym_ctx).AsCString("");
+ if (!strncmp(name, "OUTLINED_FUNCTION_",
+ sizeof("OUTLINED_FUNCTION_") - 1)) {
+ UNWIND_LOG(log,
+ "Overriding full unwind plan, using architectural default for "
+ "function {0}",
+ name);
+ return arch_default_unwind_plan_sp;
+ }
+ }
+
FuncUnwindersSP func_unwinders_sp;
if (m_sym_ctx_valid) {
func_unwinders_sp =
diff --git a/lldb/test/API/functionalities/unwind/outlined-functions/Makefile b/lldb/test/API/functionalities/unwind/outlined-functions/Makefile
new file mode 100644
index 0000000000000..22f1051530f87
--- /dev/null
+++ b/lldb/test/API/functionalities/unwind/outlined-functions/Makefile
@@ -0,0 +1 @@
+include Makefile.rules
diff --git a/lldb/test/API/functionalities/unwind/outlined-functions/TestBacktraceThroughOutlinedFunction.py b/lldb/test/API/functionalities/unwind/outlined-functions/TestBacktraceThroughOutlinedFunction.py
new file mode 100644
index 0000000000000..b6ff8fb4564f0
--- /dev/null
+++ b/lldb/test/API/functionalities/unwind/outlined-functions/TestBacktraceThroughOutlinedFunction.py
@@ -0,0 +1,54 @@
+"""
+Test that we can backtrace through an OUTLINED_FUNCTION which is called in a non-ABI way.
+"""
+
+import lldb
+import json
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class TestBacktraceThroughOutlinedFunction(TestBase):
+ NO_DEBUG_INFO_TESTCASE = True
+
+ @skipIfLLVMTargetMissing("AArch64")
+ def test_backtrace_through_outlined_epilogue(self):
+ """Test that we can backtrace through an OUTLINED_FUNCTION that has the epilogue of a function."""
+
+ target = self.dbg.CreateTarget("")
+ exe = "binary.json"
+ with open(exe) as f:
+ exe_json = json.load(f)
+ exe_uuid = exe_json["uuid"]
+
+ target.AddModule(exe, "", exe_uuid)
+ self.assertTrue(target.IsValid())
+ core = self.getBuildArtifact("core")
+ self.yaml2macho_core("arm64-outlined-epilogue-core.yaml", core, exe_uuid)
+
+ if self.TraceOn():
+ self.runCmd("log enable lldb unwind")
+
+ process = target.LoadCore(core)
+ self.assertTrue(process.IsValid())
+
+ if self.TraceOn():
+ self.runCmd("target list")
+ self.runCmd("image list")
+ self.runCmd("target modules dump sections")
+ self.runCmd("target modules dump symtab")
+ self.runCmd("bt")
+
+ thread = process.GetThreadAtIndex(0)
+ self.assertTrue(thread.IsValid())
+
+ self.assertEqual(thread.GetNumFrames(), 4)
+ stackframe_names = [
+ "foo_epilogue",
+ "OUTLINED_FUNCTION_3",
+ "sub_main_function",
+ "main",
+ ]
+ for i, name in enumerate(stackframe_names):
+ self.assertEqual(name, thread.GetFrameAtIndex(i).GetSymbol().GetName())
diff --git a/lldb/test/API/functionalities/unwind/outlined-functions/arm64-outlined-epilogue-core.yaml b/lldb/test/API/functionalities/unwind/outlined-functions/arm64-outlined-epilogue-core.yaml
new file mode 100644
index 0000000000000..910da2a8c359d
--- /dev/null
+++ b/lldb/test/API/functionalities/unwind/outlined-functions/arm64-outlined-epilogue-core.yaml
@@ -0,0 +1,98 @@
+cpu: arm64
+threads:
+ - regsets:
+ - flavor: gpr
+ registers: [{name: sp, value: 0x000000016fdfef10},
+ {name: fp, value: 0x000000016fdfef30},
+ {name: lr, value: 0x00000001000005bc},
+ {name: pc, value: 0x00000001000005c8}]
+
+memory-regions:
+ # stack memory fetched via
+ # (lldb) p/x $sp
+ # (lldb) (lldb) x/56gx $sp
+ # % pbpaste | sed -e 's,.*: ,,' -e 's/ /, /g' -e 's/$/,/'
+ - addr: 0x16fdfef10
+ UInt64: [
+ 0x0000000201294180, 0x00000002012940d8,
+ 0x000000016fdfef30, 0x00000001000004d0,
+ 0x000000016fdfef40, 0x00000001000004fc,
+ 0x000000016fdfef60, 0x000000010000052c,
+ 0x0000000201294180, 0x0000000000000000,
+ 0x000000016fdff5e0, 0x000000019b657924,
+ 0x0000000000000000, 0x0000000000000001,
+ 0x0000000000000000, 0x00000eee94dd2336,
+ 0x0000000000000000, 0x000000010000c000,
+ 0x000000019b626000, 0x000000016fdff5f0,
+ 0x0000000100104000, 0x6013e5e74dd70cbb,
+ 0x0000000100000504, 0x00000000000493e4,
+ 0x00000001000e0101, 0x0000000201294220,
+ 0x0000000042000000, 0x000000019b657f78,
+ 0x00000002031b9e60, 0x000000016fdff010,
+ 0x000000010000c000, 0x0000000000000001,
+ 0x00000002031b6480, 0x00000002012940d8,
+ 0x0000000000000000, 0x0000000000000000,
+ 0x0000000000000000, 0x0000000000000000,
+ 0x0000000000000010, 0x0000000000000007,
+ 0x0000000201294180, 0x000000016fdfefa8,
+ 0x000000016fdfefa0, 0x000000016fdff158,
+ 0x000000016fdff010, 0x000000016fdfefd8,
+ 0x000000016fdfefc0, 0x0000000000004000,
+ 0x00000001000dc000, 0x0000000000004000,
+ 0x00000001000e0000, 0x0000000000024000,
+ 0x0000000100104000, 0x0000000000070000,
+ 0x0000000000000000, 0x0000000000000000
+ ]
+
+ # Code for `foo_epilogue`
+ # 0x1000005c8 <+0>: 0xd65f03c0 ret
+ - addr: 0x1000005c8
+ UInt32: [ 0xd65f03c0 ] # ret
+
+ # Code for `OUTLINED_FUNCTION_3`
+ # 0x1000005b8 <+0>: 0x94000004 bl 0x1000005c8 ; foo_epilogue
+ # 0x1000005bc <+4>: 0xa9417bfd ldp x29, x30, [sp, #0x10]
+ # 0x1000005c0 <+8>: 0x910083ff add sp, sp, #0x20
+ # 0x1000005c4 <+12>: 0xd65f03c0 ret
+ - addr: 0x1000005b8
+ UInt32: [ 0x94000004, 0xa9417bfd, 0x910083ff, 0xd65f03c0 ]
+
+ # Code for `function_epilogue_outlined`
+ # 0x1000005ac <+0>: 0xd10083ff sub sp, sp, #0x20
+ # 0x1000005b0 <+4>: 0xa9017bfd stp x29, x30, [sp, #0x10]
+ # 0x1000005b4 <+8>: 0x14000001 b 0x1000005b8 ; OUTLINED_FUNCTION_3
+ - addr: 0x1000005ac
+ UInt32: [ 0xd10083ff, 0xa9017bfd, 0x14000001 ]
+
+ # Code for `call_external_functions`
+ # 0x1000004b0 <+0>: 0xa9bf7bfd stp x29, x30, [sp, #-0x10]!
+ # 0x1000004b4 <+4>: 0x910003fd mov x29, sp
+ # 0x1000004b8 <+8>: 0x90000000 adrp x0, 0
+ # 0x1000004bc <+12>: 0x91176000 add x0, x0, #0x5d8 ; "there"
+ # 0x1000004c0 <+16>: 0x94000043 bl 0x1000005cc ; symbol stub for: puts
+ # 0x1000004c4 <+20>: 0x94000021 bl 0x100000548 ; function_prologue_outlined
+ # 0x1000004c8 <+24>: 0x9400002c bl 0x100000578 ; function_body_outlined
+ # 0x1000004cc <+28>: 0x94000038 bl 0x1000005ac ; function_epilogue_outlined
+ # 0x1000004d0 <+32>: 0x90000000 adrp x0, 0
+ # 0x1000004d4 <+36>: 0x91177800 add x0, x0, #0x5de ; "done"
+ # 0x1000004d8 <+40>: 0x9400003d bl 0x1000005cc ; symbol stub for: puts
+ # 0x1000004dc <+44>: 0xa8c17bfd ldp x29, x30, [sp], #0x10
+ # 0x1000004e0 <+48>: 0xd65f03c0 ret
+ - addr: 0x1000004b0
+ UInt32: [ 0xa9bf7bfd, 0x910003fd, 0x90000000, 0x91176000,
+ 0x94000043, 0x94000021, 0x9400002c, 0x94000038,
+ 0x90000000, 0x91177800, 0x9400003d, 0xa8c17bfd, 0xd65f03c0 ]
+
+ # Code for `sub_main_function`
+ # 0x1000004e4 <+0>: 0xa9bf7bfd stp x29, x30, [sp, #-0x10]!
+ # 0x1000004e8 <+4>: 0x910003fd mov x29, sp
+ # 0x1000004ec <+8>: 0x90000000 adrp x0, 0
+ # 0x1000004f0 <+12>: 0x91178c00 add x0, x0, #0x5e3 ; "in subfunc"
+ # 0x1000004f4 <+16>: 0x94000036 bl 0x1000005cc ; symbol stub for: puts
+ # 0x1000004f8 <+20>: 0x97ffffee bl 0x1000004b0 ; call_external_functions at main.c:7
+ # 0x1000004fc <+24>: 0xa8c17bfd ldp x29, x30, [sp], #0x10
+ # 0x100000500 <+28>: 0xd65f03c0 ret
+ - addr: 0x1000004e4
+ UInt32: [ 0xa9bf7bfd, 0x910003fd, 0x90000000, 0x91178c00,
+ 0x94000036, 0x97ffffee, 0xa8c17bfd, 0xd65f03c0 ]
+
diff --git a/lldb/test/API/functionalities/unwind/outlined-functions/binary.json b/lldb/test/API/functionalities/unwind/outlined-functions/binary.json
new file mode 100644
index 0000000000000..559733387a4a7
--- /dev/null
+++ b/lldb/test/API/functionalities/unwind/outlined-functions/binary.json
@@ -0,0 +1,59 @@
+{
+ "triple": "arm64-apple-macosx",
+ "uuid": "743F6235-A56E-3DB3-8185-19FE820E5DF1",
+ "type": "executable",
+ "sections": [
+ {
+ "user_id": 100,
+ "name": "TEXT",
+ "type": "code",
+ "address": 4294967296,
+ "size": 16384,
+ "file_offset": 0,
+ "file_size": 16384,
+ "alignment": 2,
+ "flags": 514,
+ "read": true,
+ "write": false,
+ "execute": true
+ }
+ ],
+ "symbols": [
+ {
+ "name": "main",
+ "type": "code",
+ "size": 68,
+ "address": 4294968580
+ },
+ {
+ "name": "sub_main_function",
+ "type": "code",
+ "size": 32,
+ "address": 4294968548
+ },
+ {
+ "name": "call_external_functions",
+ "type": "code",
+ "size": 52,
+ "address": 4294968496
+ },
+ {
+ "name": "function_epilogue_outlined",
+ "type": "code",
+ "size": 12,
+ "address": 4294968748
+ },
+ {
+ "name": "OUTLINED_FUNCTION_3",
+ "type": "code",
+ "size": 16,
+ "address": 4294968760
+ },
+ {
+ "name": "foo_epilogue",
+ "type": "code",
+ "size": 4,
+ "address": 4294968776
+ }
+ ]
+}
diff --git a/lldb/test/API/functionalities/unwind/outlined-functions/main.c b/lldb/test/API/functionalities/unwind/outlined-functions/main.c
new file mode 100644
index 0000000000000..0589995f6cdac
--- /dev/null
+++ b/lldb/test/API/functionalities/unwind/outlined-functions/main.c
@@ -0,0 +1,25 @@
+#include <stdio.h>
+
+void function_prologue_outlined(void);
+void function_body_outlined(void);
+void function_epilogue_outlined(void);
+
+void call_external_functions() {
+ puts("there");
+ function_prologue_outlined();
+ function_body_outlined();
+ function_epilogue_outlined();
+ puts("done");
+}
+
+void sub_main_function() {
+ puts("in subfunc");
+ call_external_functions();
+}
+
+int main() {
+ puts("HI");
+ sub_main_function();
+ puts("exiting");
+ return 0;
+}
diff --git a/lldb/test/API/functionalities/unwind/outlined-functions/outlined-functions.s b/lldb/test/API/functionalities/unwind/outlined-functions/outlined-functions.s
new file mode 100644
index 0000000000000..24895e9d71202
--- /dev/null
+++ b/lldb/test/API/functionalities/unwind/outlined-functions/outlined-functions.s
@@ -0,0 +1,121 @@
+ .text
+// ---------------------------
+// A function with its prologue outlined
+// ---------------------------
+
+ .globl _function_prologue_outlined
+ .p2align 2
+_function_prologue_outlined:
+ .cfi_startproc
+
+ ;; Do a non-ABI call where I put the
+ ;; return address in x0 and then jump
+ ;; to an outlined function.
+ adrp x0, Lfunc_return0 at PAGE
+ add x0, x0, Lfunc_return0 at PAGEOFF
+ b _OUTLINED_FUNCTION_1
+Lfunc_return0:
+ bl _foo_prologue
+
+ nop ;; working around a bug where debugserver used to
+ ;; migrate past a builtin_debugtrap brk, it would
+ ;; do it here and now show us hitting it as we
+ ;; private step out of foo_prologue. Insert a
+ ;; nop to avoid that.
+
+ //brk #0xf000 ;; __builtin_debugtrap "middle" of function
+
+ ldp x29, x30, [sp, #16]
+ add sp, sp, #32
+ ret
+ .cfi_endproc
+
+ .globl _OUTLINED_FUNCTION_1
+_OUTLINED_FUNCTION_1:
+ .cfi_startproc
+ sub sp, sp, #32
+ stp x29, x30, [sp, #16]
+ br x0
+ .cfi_endproc
+
+ .globl _foo_prologue
+_foo_prologue:
+ ret
+
+
+// ---------------------------
+// A function with part of its body outlined
+// ---------------------------
+
+
+ .globl _function_body_outlined
+ .p2align 2
+_function_body_outlined:
+ .cfi_startproc
+ sub sp, sp, #32
+ stp x29, x30, [sp, #16]
+ .cfi_def_cfa_offset 32
+ .cfi_offset w30, -8
+ .cfi_offset w29, -16
+
+ bl _OUTLINED_FUNCTION_2
+
+ //brk #0xf000 ;; __builtin_debugtrap "middle" of function
+
+ ldp x29, x30, [sp, #16]
+ add sp, sp, #32
+ ret
+ .cfi_endproc
+
+ ;; This OUTLINED_FUNCTION creates a normal
+ ;; stack frame, but doesn't include unwind
+ ;; instructions to that effect in the DWARF.
+ ;;
+ ;; We intend for the debug instructions to be wrong,
+ ;; but an instruction analysis unwind plan would do the
+ ;; right thing here.
+ .globl _OUTLINED_FUNCTION_2
+_OUTLINED_FUNCTION_2:
+ .cfi_startproc
+ sub sp, sp, #32
+ stp x29, x30, [sp, #16]
+ bl _foo_midfunction
+ ldp x29, x30, [sp, #16]
+ add sp, sp, #32
+ ret
+ .cfi_endproc
+
+ .globl _foo_midfunction
+_foo_midfunction:
+ ret
+
+// ---------------------------
+// A function with its epilogue outlined
+// ---------------------------
+
+ .globl _function_epilogue_outlined
+ .p2align 2
+_function_epilogue_outlined:
+ .cfi_startproc
+ sub sp, sp, #32
+ stp x29, x30, [sp, #16]
+ .cfi_def_cfa_offset 32
+ .cfi_offset w30, -8
+ .cfi_offset w29, -16
+
+ //brk #0xf000 ;; __builtin_debugtrap "middle" of function
+ b _OUTLINED_FUNCTION_3
+ .cfi_endproc
+
+ .globl _OUTLINED_FUNCTION_3
+_OUTLINED_FUNCTION_3:
+ .cfi_startproc
+ bl _foo_epilogue
+ ldp x29, x30, [sp, #16]
+ add sp, sp, #32
+ ret
+ .cfi_endproc
+
+ .globl _foo_epilogue
+_foo_epilogue:
+ ret
>From 31e4e1be694c19de8a63538a20fcdc83a64bf25b Mon Sep 17 00:00:00 2001
From: Jason Molenda <jmolenda at apple.com>
Date: Thu, 18 Jun 2026 18:06:32 -0700
Subject: [PATCH 2/4] Typeo fix. Add live process debug on Darwin systems.
On a Darwin system, compile & run the .c and .s file into a binary.
Do a backtrace from two points: below a mid-function outlining,
and below an epilogue outlining, and verify that the backtrace looks
like what we expect when we use the architectural default unwind
plan to unwind from an OUTLINED_FUNCTION.
---
lldb/source/Target/RegisterContextUnwind.cpp | 2 +-
.../unwind/outlined-functions/Makefile | 12 ++++
.../TestBacktraceThroughOutlinedFunction.py | 62 ++++++++++++++++++-
3 files changed, 72 insertions(+), 4 deletions(-)
diff --git a/lldb/source/Target/RegisterContextUnwind.cpp b/lldb/source/Target/RegisterContextUnwind.cpp
index 4aef810a7d23a..584e5a1abfac7 100644
--- a/lldb/source/Target/RegisterContextUnwind.cpp
+++ b/lldb/source/Target/RegisterContextUnwind.cpp
@@ -875,7 +875,7 @@ RegisterContextUnwind::GetFullUnwindPlanForFrame() {
//
// The call to the OUTLINED_FUNCTION may not be a normal ABI call (e.g.
// on RISCV it might be called `jal t0, OUTLINED_FUNCTION_<nn>` putting the
- // return address in a temporary register instead of $ra). Tthe unwind
+ // return address in a temporary register instead of $ra). The unwind
// instructions in eh_frame/debug_frame are not correct today for an
// OUTLINED_FUNCTION, even when a normal ABI call is made.
// Clang does not insert CFI directives in the OUTLINED_FUNCTION so it
diff --git a/lldb/test/API/functionalities/unwind/outlined-functions/Makefile b/lldb/test/API/functionalities/unwind/outlined-functions/Makefile
index 22f1051530f87..d4e2c3172a918 100644
--- a/lldb/test/API/functionalities/unwind/outlined-functions/Makefile
+++ b/lldb/test/API/functionalities/unwind/outlined-functions/Makefile
@@ -1 +1,13 @@
+C_SOURCES := main.c
+
+outlined-functions.o: outlined-functions.s
+ $(CPP) -o outlined-functions.s $(SRCDIR)/outlined-functions.s
+ $(CC) $(CFLAGS) -c -o outlined-functions.o outlined-functions.s
+
include Makefile.rules
+
+a.out: outlined-functions.o
+
+# Needs to come after include
+OBJECTS += outlined-functions.o
+
diff --git a/lldb/test/API/functionalities/unwind/outlined-functions/TestBacktraceThroughOutlinedFunction.py b/lldb/test/API/functionalities/unwind/outlined-functions/TestBacktraceThroughOutlinedFunction.py
index b6ff8fb4564f0..1225d7d7f800b 100644
--- a/lldb/test/API/functionalities/unwind/outlined-functions/TestBacktraceThroughOutlinedFunction.py
+++ b/lldb/test/API/functionalities/unwind/outlined-functions/TestBacktraceThroughOutlinedFunction.py
@@ -13,8 +13,8 @@ class TestBacktraceThroughOutlinedFunction(TestBase):
NO_DEBUG_INFO_TESTCASE = True
@skipIfLLVMTargetMissing("AArch64")
- def test_backtrace_through_outlined_epilogue(self):
- """Test that we can backtrace through an OUTLINED_FUNCTION that has the epilogue of a function."""
+ def test_corefile_backtrace(self):
+ """Corefile test that we can backtrace through an OUTLINED_FUNCTION that has the epilogue of a function."""
target = self.dbg.CreateTarget("")
exe = "binary.json"
@@ -38,12 +38,68 @@ def test_backtrace_through_outlined_epilogue(self):
self.runCmd("image list")
self.runCmd("target modules dump sections")
self.runCmd("target modules dump symtab")
+ self.backtrace_through_outlined_epilogue(process)
+
+ @skipIf(oslist=no_match([lldbplatformutil.getDarwinOSTriples()]))
+ @skipIf(archs=no_match(["aarch64", "arm64", "arm64e"]))
+ def test_live_process_backtrace(self):
+ """Live process test that we can backtrace through two OUTLINED_FUNCTIONs with incorrect unwind instructions."""
+ self.build()
+ target, process, thread, bp = lldbutil.run_to_name_breakpoint(
+ self, "foo_midfunction"
+ )
+
+ self.assertTrue(target.IsValid())
+ self.assertTrue(process.IsValid())
+
+ self.backtrace_through_outlined_midfunction(process)
+
+ target.BreakpointCreateByName("foo_epilogue")
+ process.Continue()
+ self.assertTrue(process.IsValid())
+
+ self.backtrace_through_outlined_epilogue(process)
+
+ # Test for the behavior of an architectural default unwind plan
+ # getting above OUTLINED_FUNCTION_2. The OUTLINED_FUNCTION_2
+ # DWARF debug_frame/compact unwind instructions are invalid and
+ # the entire stack frame will be rejected by the unwinder -- the
+ # OUTLINED_FUNCTION_2 won't appear on the backtrace.
+ def backtrace_through_outlined_midfunction(self, process):
+ if self.TraceOn():
+ self.runCmd("bt")
+
+ thread = process.GetThreadAtIndex(0)
+ self.assertTrue(thread.IsValid())
+
+ important_frames = 4
+ # We may have a dyld `start` stack frame, or not; not relevant
+ # to this test.
+ self.assertGreaterEqual(thread.GetNumFrames(), important_frames)
+ stackframe_names = [
+ "foo_midfunction",
+ "OUTLINED_FUNCTION_2",
+ "sub_main_function",
+ "main",
+ ]
+ for i, name in enumerate(stackframe_names):
+ self.assertEqual(name, thread.GetFrameAtIndex(i).GetSymbol().GetName())
+
+ # Test for the behavior of an architectural default unwind plan
+ # getting above OUTLINED_FUNCTION_3. If we do an instruction analysis
+ # unwind we'll have a duplication of OUTLINED_FUNCTION_3 in the
+ # backtrace.
+ def backtrace_through_outlined_epilogue(self, process):
+ if self.TraceOn():
self.runCmd("bt")
thread = process.GetThreadAtIndex(0)
self.assertTrue(thread.IsValid())
- self.assertEqual(thread.GetNumFrames(), 4)
+ important_frames = 4
+ # We may have a dyld `start` stack frame, or not; not relevant
+ # to this test.
+ self.assertGreaterEqual(thread.GetNumFrames(), important_frames)
stackframe_names = [
"foo_epilogue",
"OUTLINED_FUNCTION_3",
>From 14b1a0e341bfca16b61589b9e3d456eb29c2a201 Mon Sep 17 00:00:00 2001
From: Jason Molenda <github-mail at molenda.com>
Date: Thu, 25 Jun 2026 17:15:05 -0700
Subject: [PATCH 3/4] Update lldb/source/Target/RegisterContextUnwind.cpp
Co-authored-by: Jonas Devlieghere <jonas at devlieghere.com>
---
lldb/source/Target/RegisterContextUnwind.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lldb/source/Target/RegisterContextUnwind.cpp b/lldb/source/Target/RegisterContextUnwind.cpp
index 584e5a1abfac7..33a97809fbad3 100644
--- a/lldb/source/Target/RegisterContextUnwind.cpp
+++ b/lldb/source/Target/RegisterContextUnwind.cpp
@@ -878,7 +878,7 @@ RegisterContextUnwind::GetFullUnwindPlanForFrame() {
// return address in a temporary register instead of $ra). The unwind
// instructions in eh_frame/debug_frame are not correct today for an
// OUTLINED_FUNCTION, even when a normal ABI call is made.
- // Clang does not insert CFI directives in the OUTLINED_FUNCTION so it
+ // CFI may be absent or incorrect. So it
// will look like a frameless function that does nothing beyond its initial
// unwind state.
//
>From 95dc2198df07d69496ca50dce6943735a80dcbdc Mon Sep 17 00:00:00 2001
From: Jason Molenda <jmolenda at apple.com>
Date: Thu, 25 Jun 2026 17:46:50 -0700
Subject: [PATCH 4/4] Create a static IsClangOutlinedFunction() helper
function.
---
lldb/source/Target/RegisterContextUnwind.cpp | 33 ++++++++++----------
1 file changed, 17 insertions(+), 16 deletions(-)
diff --git a/lldb/source/Target/RegisterContextUnwind.cpp b/lldb/source/Target/RegisterContextUnwind.cpp
index 33a97809fbad3..f43886997f951 100644
--- a/lldb/source/Target/RegisterContextUnwind.cpp
+++ b/lldb/source/Target/RegisterContextUnwind.cpp
@@ -60,6 +60,20 @@ static bool CallFrameAddressIsValid(ABISP abi_sp, lldb::addr_t cfa) {
return cfa != 0 && cfa != 1;
}
+/// Identify a clang outlined function by symbol name.
+///
+/// The unwind information in outlined functions from clang can be
+/// incorrect, and because of when the outlining happens in the compilation,
+/// it may not be possible to fix. We will need to ignore any
+/// instruction-emulation or compiler-sourced unwind plans for these
+/// functions, and fall back to an ABI default unwindplan.
+static bool IsClangOutlinedFunction(const SymbolContext &sym_ctx) {
+ llvm::StringRef name = GetSymbolOrFunctionName(sym_ctx).StringRef();
+ if (name.starts_with("OUTLINED_FUNCTION_"))
+ return true;
+ return false;
+}
+
#define UNWIND_LOG_IMPL(LOG_FN, log, ...) \
LOG_FN(log, "{0}th{1}/fr{2} {3}", \
llvm::indent(std::min(m_frame_number, 100U)), m_thread.GetIndexID(), \
@@ -878,23 +892,10 @@ RegisterContextUnwind::GetFullUnwindPlanForFrame() {
// return address in a temporary register instead of $ra). The unwind
// instructions in eh_frame/debug_frame are not correct today for an
// OUTLINED_FUNCTION, even when a normal ABI call is made.
- // CFI may be absent or incorrect. So it
- // will look like a frameless function that does nothing beyond its initial
- // unwind state.
- //
- // A function may outline its prologue, mid-function block of instructions,
- // or its epilogue. If a function's prologue is outlined, the unwind
- // instructions (eh_frame/debug_frame/lldb's instruction analysis) for the
- // original function are incorrect and cannot be trusted.
- //
- // If we are in an OUTLINED_FUNCTION in a backtrace, distrust all sources
- // of unwind information and use the architectural default unwind plan
- // instead. We may miss the caller stack frame in the backtrace, but it
- // will work more reliably than any other method.
+ // CFI may be absent or incorrect; instruction emulation may be incorrect
+ // because it assumes a normal ABI call was made.
if (m_sym_ctx_valid && arch_default_unwind_plan_sp) {
- const char *name = GetSymbolOrFunctionName(m_sym_ctx).AsCString("");
- if (!strncmp(name, "OUTLINED_FUNCTION_",
- sizeof("OUTLINED_FUNCTION_") - 1)) {
+ if (IsClangOutlinedFunction(m_sym_ctx) {
UNWIND_LOG(log,
"Overriding full unwind plan, using architectural default for "
"function {0}",
More information about the lldb-commits
mailing list