https://github.com/jasonmolenda created 
https://github.com/llvm/llvm-project/pull/204500

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

>From 6c1d201c00e4320da57ef11dafc65a58a7221bf9 Mon Sep 17 00:00:00 2001
From: Jason Molenda <[email protected]>
Date: Wed, 17 Jun 2026 18:59:25 -0700
Subject: [PATCH] [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@PAGE
+  add x0, x0, Lfunc_return0@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

_______________________________________________
lldb-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits

Reply via email to