[Lldb-commits] [lldb] [lldb] Don't trust unwind information for outlined functions (PR #204500)
via lldb-commits
lldb-commits at lists.llvm.org
Wed Jun 17 19:01:36 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-lldb
Author: Jason Molenda (jasonmolenda)
<details>
<summary>Changes</summary>
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
---
Full diff: https://github.com/llvm/llvm-project/pull/204500.diff
7 Files Affected:
- (modified) lldb/source/Target/RegisterContextUnwind.cpp (+35)
- (added) lldb/test/API/functionalities/unwind/outlined-functions/Makefile (+1)
- (added) lldb/test/API/functionalities/unwind/outlined-functions/TestBacktraceThroughOutlinedFunction.py (+54)
- (added) lldb/test/API/functionalities/unwind/outlined-functions/arm64-outlined-epilogue-core.yaml (+98)
- (added) lldb/test/API/functionalities/unwind/outlined-functions/binary.json (+59)
- (added) lldb/test/API/functionalities/unwind/outlined-functions/main.c (+25)
- (added) lldb/test/API/functionalities/unwind/outlined-functions/outlined-functions.s (+121)
``````````diff
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
``````````
</details>
https://github.com/llvm/llvm-project/pull/204500
More information about the lldb-commits
mailing list