Author: Karim Alweheshy Date: 2026-05-26T10:20:13-07:00 New Revision: bacd876134494c69d02b4509e125c47b5e86d3d2
URL: https://github.com/llvm/llvm-project/commit/bacd876134494c69d02b4509e125c47b5e86d3d2 DIFF: https://github.com/llvm/llvm-project/commit/bacd876134494c69d02b4509e125c47b5e86d3d2.diff LOG: [lld][MachO] Preserve __eh_frame ordering during BP section sorting (#191412) The Balanced Partitioning section orderer collects all live `ConcatInputSection`s as candidates for content-similarity reordering. This includes `__eh_frame` CIE and FDE records, which have internal ordering constraints: each FDE contains a backward-relative 32-bit offset to its parent CIE, requiring CIEs to precede their FDEs. When the BP orderer assigns priorities to `__eh_frame` subsections and `Writer.cpp` sorts by those priorities, FDEs can end up before their parent CIEs. The resulting CIE-pointer offsets resolve correctly with 32-bit wrapping arithmetic but underflow with 64-bit pointer arithmetic, causing DWARF consumers (crash reporters, debuggers) to silently lose unwind data. ## Fix Have the BP orderer skip the Mach-O `__TEXT,__eh_frame` section explicitly before collecting candidate subsections, preserving the existing CIE/FDE order without adding state to `Section`. This is the only MachO section with this constraint: - `__unwind_info` is a `SyntheticSection` (not a `ConcatOutputSection`), so it never enters the BP pipeline - `__gcc_except_tab` LSDA entries are referenced by absolute offset, so reordering is safe - `-order_file` is unaffected because it assigns priorities through symbol definitions (which live in `__text`, not `__eh_frame`). Only the BP orderer enumerates sections directly. ## Production impact Verified on a large iOS application (~218 MB binary, ~34,000 FDEs) linked with `lld` + `--bp-compression-sort=both`. ### Static analysis of `__eh_frame` Simulating 64-bit CIE pointer resolution on the output binary across multiple builds: | Build | `__eh_frame` layout | FDEs resolved (64-bit) | FDEs failed | |---|---|---|---| | lld + BP sort | FDEs first | 15 / 34,257 | **34,242 (99.96%)** | | lld + fix | CIEs first | 30,558 / 30,558 | **0** | ### Runtime verification Proxied crash report uploads from a device running both the affected and fixed binaries: | | Affected build | Fixed build | |---|---|---| | Threads captured | 3 | 24 | | Total frames | 28 | 135 | | Background threads | 0-2 | 23 | The affected build lost ~85% of thread data. The crash reporter could only unwind the crashed thread (via compact unwind). All background thread unwind data was silently dropped. ## Reproducer Minimal test case (ARM64). Requires `--bp-compression-sort=both` to trigger: ```bash llvm-mc -filetype=obj -emit-compact-unwind-non-canonical=true \ -triple=arm64-apple-macos11.0 test.s -o test.o ld64.lld -arch arm64 -platform_version macos 11.0 11.0 \ -syslibroot $(xcrun --show-sdk-path) -lSystem -lc++ \ test.o -o test --bp-compression-sort=both llvm-objdump --dwarf=frames test # Without fix: "error: parsing FDE data at 0x0 failed due to missing CIE" # With fix: CIE records correctly precede their FDEs ``` Standalone reproducer with 64-bit CIE simulation script: https://gist.github.com/karim-alweheshy/ae28196c4fbb295f81cc793cfbe0c1b7 ## Test The lit test creates multiple functions with `cfi_escape` (forcing DWARF unwind mode) and different personality functions (producing separate CIEs), then links with `--bp-compression-sort=both`. Verified on both x86_64 and arm64. Made with [Cursor](https://cursor.com) --------- Co-authored-by: Karim Alweheshy <[email protected]> Co-authored-by: Ellis Hoag <[email protected]> Added: lld/test/MachO/eh-frame-ordering.s Modified: lld/MachO/BPSectionOrderer.cpp Removed: ################################################################################ diff --git a/lld/MachO/BPSectionOrderer.cpp b/lld/MachO/BPSectionOrderer.cpp index a9b5d07ac55e5..11f48278a64bb 100644 --- a/lld/MachO/BPSectionOrderer.cpp +++ b/lld/MachO/BPSectionOrderer.cpp @@ -8,6 +8,7 @@ #include "BPSectionOrderer.h" #include "InputSection.h" +#include "OutputSegment.h" #include "Relocations.h" #include "Symbols.h" #include "lld/Common/BPSectionOrdererBase.inc" @@ -122,6 +123,9 @@ DenseMap<const InputSection *, int> lld::macho::runBalancedPartitioning( DenseMap<CachedHashStringRef, std::set<unsigned>> rootSymbolToSectionIdxs; for (const auto *file : inputFiles) { for (auto *sec : file->sections) { + if (sec->name == section_names::ehFrame && + sec->segname == segment_names::text) + continue; for (auto &subsec : sec->subsections) { auto *isec = subsec.isec; if (!isec || isec->data.empty() || !isec->data.data()) diff --git a/lld/test/MachO/eh-frame-ordering.s b/lld/test/MachO/eh-frame-ordering.s new file mode 100644 index 0000000000000..8c6d9986b5f9f --- /dev/null +++ b/lld/test/MachO/eh-frame-ordering.s @@ -0,0 +1,107 @@ +# REQUIRES: x86, aarch64 +## Test that __eh_frame CIE/FDE ordering is preserved even when +## priority-based section sorting (from BP compression sort, order files, +## etc.) would otherwise reorder input sections. CIE records must precede +## the FDE records that reference them; reordering breaks CIE pointer +## resolution. + +## x86_64 +# RUN: llvm-mc -filetype=obj -emit-compact-unwind-non-canonical=true -triple=x86_64-apple-macos10.15 %s -o %t-x86_64.o +# RUN: %lld -lSystem -lc++ %t-x86_64.o -o %t-x86_64 --bp-compression-sort=both +# RUN: llvm-objdump --dwarf=frames %t-x86_64 2>&1 | FileCheck %s --implicit-check-not=error --implicit-check-not=warning + +## arm64 +# RUN: llvm-mc -filetype=obj -emit-compact-unwind-non-canonical=true -triple=arm64-apple-macos11.0 %s -o %t-arm64.o +# RUN: %lld -arch arm64 -lSystem -lc++ %t-arm64.o -o %t-arm64 --bp-compression-sort=both +# RUN: llvm-objdump --dwarf=frames %t-arm64 2>&1 | FileCheck %s --implicit-check-not=error --implicit-check-not=warning + +## Verify that __eh_frame starts with a CIE (not an FDE), contains both +## CIEs and FDEs, and that no parse errors occur. The test uses two +## personality functions, producing two CIE groups (CIE_A + FDEs, CIE_B + +## FDEs). The --implicit-check-not flags above ensure no FDE fails to +## resolve its CIE pointer. +# CHECK: .eh_frame contents: +# CHECK: {{[0-9a-f]+}} {{.*}} CIE +# CHECK: {{[0-9a-f]+}} {{.*}} FDE +# CHECK: {{[0-9a-f]+}} {{.*}} FDE +# CHECK: {{[0-9a-f]+}} {{.*}} FDE +# CHECK: {{[0-9a-f]+}} {{.*}} FDE + +.globl _my_personality_a, _my_personality_b, _main + +.text +## _func_a uses cfi_escape to force DWARF unwind (can't be compact-encoded). +## Uses personality A -> produces CIE_A + FDE for _func_a. +.p2align 2 +_func_a: + .cfi_startproc + .cfi_personality 155, _my_personality_a + .cfi_lsda 16, Lexception_a + .cfi_def_cfa_offset 16 + .cfi_escape 0x2e, 0x10 + ret + .cfi_endproc + +## _func_b also uses personality A + cfi_escape -> reuses CIE_A, new FDE. +.p2align 2 +_func_b: + .cfi_startproc + .cfi_personality 155, _my_personality_a + .cfi_lsda 16, Lexception_b + .cfi_def_cfa_offset 16 + .cfi_escape 0x2e, 0x10 + ret + .cfi_endproc + +## _func_c uses personality B + cfi_escape -> produces CIE_B + FDE. +.p2align 2 +_func_c: + .cfi_startproc + .cfi_personality 155, _my_personality_b + .cfi_lsda 16, Lexception_c + .cfi_def_cfa_offset 16 + .cfi_escape 0x2e, 0x10 + ret + .cfi_endproc + +## _func_d uses personality B + cfi_escape -> reuses CIE_B, new FDE. +.p2align 2 +_func_d: + .cfi_startproc + .cfi_personality 155, _my_personality_b + .cfi_lsda 16, Lexception_d + .cfi_def_cfa_offset 16 + .cfi_escape 0x2e, 0x10 + ret + .cfi_endproc + +.p2align 2 +_my_personality_a: + ret + +.p2align 2 +_my_personality_b: + ret + +.p2align 2 +_main: + ret + +.section __TEXT,__gcc_except_tab +GCC_except_table_a: +Lexception_a: + .byte 255 + +GCC_except_table_b: +Lexception_b: + .byte 255 + +GCC_except_table_c: +Lexception_c: + .byte 255 + +GCC_except_table_d: +Lexception_d: + .byte 255 + +.subsections_via_symbols _______________________________________________ llvm-branch-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-branch-commits
