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
