[lld] [lld][ELF] refresh .eh_frame_hdr cache (PR #226402)
Martin Levesque via llvm-commits
llvm-commits at lists.llvm.org
Fri Sep 25 01:58:07 PDT 2026
https://github.com/wehzzz created https://github.com/llvm/llvm-project/pull/226402
Fixes https://github.com/llvm/llvm-project/issues/226166
>From f831ff8ad936f8ab7b7cafc2e3e5e4718e1baea4 Mon Sep 17 00:00:00 2001
From: Martin Levesque <martin.levesque at datadoghq.com>
Date: Fri, 25 Sep 2026 08:53:43 +0000
Subject: [PATCH] [lld][ELF] refresh .eh_frame_hdr cache
---
lld/ELF/SyntheticSections.cpp | 86 ++++++++++++-------
lld/ELF/SyntheticSections.h | 20 ++++-
lld/ELF/Writer.cpp | 11 +++
lld/test/ELF/eh-frame-hdr-cfi-jump-tables.s | 81 +++++++++++++++++
lld/test/ELF/eh-frame-hdr-optimize-bb-jumps.s | 39 +++++++++
5 files changed, 200 insertions(+), 37 deletions(-)
create mode 100644 lld/test/ELF/eh-frame-hdr-cfi-jump-tables.s
create mode 100644 lld/test/ELF/eh-frame-hdr-optimize-bb-jumps.s
diff --git a/lld/ELF/SyntheticSections.cpp b/lld/ELF/SyntheticSections.cpp
index f59e676c2e6f5..67c7b5c719225 100644
--- a/lld/ELF/SyntheticSections.cpp
+++ b/lld/ELF/SyntheticSections.cpp
@@ -40,6 +40,7 @@
#include "llvm/Support/Parallel.h"
#include "llvm/Support/TimeProfiler.h"
#include <cinttypes>
+#include <cstddef>
#include <cstdlib>
using namespace llvm;
@@ -394,8 +395,9 @@ void EhFrameSection::writeTo(uint8_t *buf) {
if (!hdr || !hdr->getParent())
return;
- // Write the .eh_frame_hdr section using cached FDE data from updateAllocSize.
- bool large = hdr->large;
+ // Write the .eh_frame_hdr section using cached FDE data computed by
+ // computeFdeTable().
+ bool large = hdr->cache.large;
int64_t ehFramePtr = getParent()->addr - hdr->getVA() - 4;
auto writeField = [&](uint8_t *buf, uint64_t val) {
large ? write64(ctx, buf, val) : write32(ctx, buf, val);
@@ -413,9 +415,9 @@ void EhFrameSection::writeTo(uint8_t *buf) {
hdrBuf += 4;
writeField(hdrBuf, ehFramePtr);
hdrBuf += large ? 8 : 4;
- write32(ctx, hdrBuf, hdr->fdes.size());
+ write32(ctx, hdrBuf, hdr->cache.fdes.size());
hdrBuf += 4;
- for (const FdeData &fde : hdr->fdes) {
+ for (const FdeData &fde : hdr->cache.fdes) {
writeField(hdrBuf, fde.pcRel);
writeField(hdrBuf + (large ? 8 : 4), fde.fdeVARel);
hdrBuf += large ? 16 : 8;
@@ -433,27 +435,26 @@ bool EhFrameHeader::isNeeded() const {
return isLive() && ctx.in.ehFrame->isNeeded();
}
+static size_t ehFrameHdrSize(bool large, size_t numFdes) {
+ return 4 + (large ? 8 : 4) + 4 + numFdes * (large ? 16 : 8);
+}
+
void EhFrameHeader::finalizeContents() {
// Compute size: 4-byte header + eh_frame_ptr + fde_count + FDE table.
- // Initially `large` is false; updateAllocSize may set it to true if addresses
- // exceed the 32-bit range, then call finalizeContents again.
- auto numFdes = ctx.in.ehFrame->numFdes;
- size = 4 + (large ? 8 : 4) + 4 + numFdes * (large ? 16 : 8);
+ size = ehFrameHdrSize(cache.large, ctx.in.ehFrame->numFdes);
}
-bool EhFrameHeader::updateAllocSize(Ctx &ctx) {
- // This is called after `finalizeSynthetic`, so in the typical case without
- // .relr.dyn, this function will not change the size and assignAddresses
- // will not need another iteration.
+// Shared FDE-table computation for updateAllocSize() and refreshCache().
+EhFrameHeader::CachedFdeTable EhFrameHeader::computeFdeTable(Ctx &ctx) {
+ CachedFdeTable c;
EhFrameSection *ehFrame = ctx.in.ehFrame.get();
uint64_t hdrVA = getVA();
int64_t ehFramePtr = ehFrame->getParent()->addr - hdrVA - 4;
// Determine if 64-bit encodings are needed.
- bool newLarge = !isInt<32>(ehFramePtr);
+ c.large = !isInt<32>(ehFramePtr);
// Collect FDE entries. For each FDE, compute pcRel and fdeVARel relative to
// .eh_frame_hdr's VA.
- fdes.clear();
for (CieRecord *rec : ehFrame->getCieRecords()) {
uint8_t enc = getFdeEncoding(rec->cie);
if ((enc & 0x70) != DW_EH_PE_absptr && (enc & 0x70) != DW_EH_PE_pcrel) {
@@ -468,38 +469,57 @@ bool EhFrameHeader::updateAllocSize(Ctx &ctx) {
assert(isa<Defined>(reloc.sym) && "isFdeLive should have checked this");
int64_t pcRel = reloc.sym->getVA(ctx) + reloc.addend - hdrVA;
int64_t fdeVARel = ehFrame->getParent()->addr + fde->outputOff - hdrVA;
- fdes.push_back({pcRel, fdeVARel});
- newLarge |= !isInt<32>(pcRel) || !isInt<32>(fdeVARel);
+ c.fdes.push_back({pcRel, fdeVARel});
+ c.large |= !isInt<32>(pcRel) || !isInt<32>(fdeVARel);
}
}
// Sort the FDE list by their PC and uniquify. Usually there is only one FDE
// at an address, but there can be more than one FDEs pointing to the address.
- llvm::stable_sort(
- fdes, [](const EhFrameSection::FdeData &a,
- const EhFrameSection::FdeData &b) { return a.pcRel < b.pcRel; });
- fdes.erase(llvm::unique(fdes,
- [](const EhFrameSection::FdeData &a,
- const EhFrameSection::FdeData &b) {
- return a.pcRel == b.pcRel;
- }),
- fdes.end());
- ehFrame->numFdes = fdes.size();
-
- large = newLarge;
-
- // Compute size.
+ llvm::stable_sort(c.fdes, [](const EhFrameSection::FdeData &a,
+ const EhFrameSection::FdeData &b) {
+ return a.pcRel < b.pcRel;
+ });
+ c.fdes.erase(llvm::unique(c.fdes,
+ [](const EhFrameSection::FdeData &a,
+ const EhFrameSection::FdeData &b) {
+ return a.pcRel == b.pcRel;
+ }),
+ c.fdes.end());
+ c.requiredSize = ehFrameHdrSize(c.large, c.fdes.size());
+ return c;
+}
+
+bool EhFrameHeader::updateAllocSize(Ctx &ctx) {
+ // This is called after `finalizeSynthetic`, so in the typical case without
+ // .relr.dyn, this function will not change the size and assignAddresses
+ // will not need another iteration.
size_t oldSize = size;
- finalizeContents();
+ EhFrameSection *ehFrame = ctx.in.ehFrame.get();
+ cache = computeFdeTable(ctx);
+ ehFrame->numFdes = cache.fdes.size();
// Don't allow the section to shrink; otherwise the size of the section can
// oscillate infinitely.
- if (size < oldSize)
- size = oldSize;
+ size = std::max(cache.requiredSize, oldSize);
return size != oldSize;
}
+void EhFrameHeader::refreshCache(Ctx &ctx) {
+ // Layout is frozen here, so the allocation cannot grow. A narrower table
+ // is fine. A table that does not fit is rejected.
+ CachedFdeTable c = computeFdeTable(ctx);
+ if (c.requiredSize > size) {
+ Err(ctx) << ".eh_frame_hdr needs " << c.requiredSize << " bytes but "
+ << size
+ << " were allocated; layout moved FDEs after the header was sized";
+ return;
+ }
+ ctx.in.ehFrame->numFdes = c.fdes.size();
+ cache = std::move(c);
+}
+
GotSection::GotSection(Ctx &ctx)
: SyntheticSection(ctx, ".got", SHT_PROGBITS, SHF_ALLOC | SHF_WRITE,
ctx.target->gotEntrySize) {
diff --git a/lld/ELF/SyntheticSections.h b/lld/ELF/SyntheticSections.h
index 85596e1339545..e3f737b35364a 100644
--- a/lld/ELF/SyntheticSections.h
+++ b/lld/ELF/SyntheticSections.h
@@ -103,11 +103,23 @@ class EhFrameHeader final : public SyntheticSection {
void finalizeContents() override;
bool updateAllocSize(Ctx &) override;
- // Cached FDE data computed by updateAllocSize, used by
- // EhFrameSection::writeTo.
- SmallVector<EhFrameSection::FdeData, 0> fdes;
- bool large = false; // Whether to use sdata8 encoding.
+ // Rebuild the cached search-table entries from the current addresses.
+ // Never changes size and never relayouts. If the rebuilt table does not
+ // fit the assigned capacity, reports an error.
+ void refreshCache(Ctx &);
+
+ // Cached FDE data computed by computeFdeTable(), used by
+ // EhFrameSection::writeTo().
+ struct CachedFdeTable {
+ SmallVector<EhFrameSection::FdeData, 0> fdes;
+ bool large = false; // Whether to use sdata8 encoding.
+ size_t requiredSize = 0;
+ };
+ CachedFdeTable cache;
size_t size = 0;
+
+private:
+ CachedFdeTable computeFdeTable(Ctx &);
};
class GotSection final : public SyntheticSection {
diff --git a/lld/ELF/Writer.cpp b/lld/ELF/Writer.cpp
index 4fac47b548936..2de94ae2b69c7 100644
--- a/lld/ELF/Writer.cpp
+++ b/lld/ELF/Writer.cpp
@@ -1664,6 +1664,12 @@ template <class ELFT> void Writer<ELFT>::finalizeAddressDependentContent() {
// Sizes are no longer allowed to grow, so all allowable spills have been
// taken. Remove any leftover potential spills.
ctx.script->erasePotentialSpillSections();
+
+ // Refresh the header from the final addresses as relaxCFIJumpTables() can
+ // move input sections without changing output-section start addresses, so the
+ // cache from inside the loop can be stale (#226166).
+ if (!errCount(ctx) && ctx.in.ehFrameHdr && ctx.in.ehFrameHdr->isNeeded())
+ ctx.in.ehFrameHdr->refreshCache(ctx);
}
// If Input Sections have been shrunk (basic block sections) then
@@ -1749,6 +1755,11 @@ template <class ELFT> void Writer<ELFT>::optimizeBasicBlockJumps() {
for (OutputSection *osec : ctx.outputSections)
for (InputSection *is : getInputSections(*osec, storage))
is->trim();
+
+ // The jump deletions above shrank input sections and moved symbol values.
+ // Refresh the header so its keys match the final PCs.
+ if (ctx.in.ehFrameHdr && ctx.in.ehFrameHdr->isNeeded())
+ ctx.in.ehFrameHdr->refreshCache(ctx);
}
// Sections that finalizeAddressDependentContent may add to.
diff --git a/lld/test/ELF/eh-frame-hdr-cfi-jump-tables.s b/lld/test/ELF/eh-frame-hdr-cfi-jump-tables.s
new file mode 100644
index 0000000000000..7a41839f44fe4
--- /dev/null
+++ b/lld/test/ELF/eh-frame-hdr-cfi-jump-tables.s
@@ -0,0 +1,81 @@
+# REQUIRES: x86
+## Check that .eh_frame_hdr search-table keys match the final PCs of the
+## FDEs they reference after CFI jump-table relaxation (issue #226166).
+
+# RUN: llvm-mc -filetype=obj -triple=x86_64 %s -o %t.o
+# RUN: ld.lld --eh-frame-hdr -O2 %t.o -shared -o %t
+# RUN: llvm-readelf --unwind %t | FileCheck %s
+# RUN: ld.lld --eh-frame-hdr -O1 --branch-to-branch %t.o -shared -o %t2
+# RUN: llvm-readelf --unwind %t2 | FileCheck %s
+
+# CHECK: fde_count: 1
+# CHECK: entry 0 {
+# CHECK-NEXT: initial_location: [[PC:0x[0-9a-f]+]]
+# CHECK-NEXT: address: [[FDE:0x[0-9a-f]+]]
+# CHECK: [{{ *}}[[FDE]]{{ *}}] FDE
+# CHECK-NEXT: initial_location: [[PC]]
+
+.section .text.jt,"ax", at llvm_cfi_jump_table,8
+.type f1, at function
+f1:
+ jmp f1.cfi
+ .balign 8, 0xcc
+.type f2, at function
+f2:
+ jmp f2.cfi
+ .balign 8, 0xcc
+.type f3, at function
+f3:
+ jmp f3.cfi
+ .balign 8, 0xcc
+.type f4, at function
+f4:
+ jmp f4.cfi
+ .balign 8, 0xcc
+
+.section .text.f1,"ax", at progbits
+f1.cfi:
+ ret
+ nop
+ nop
+ nop
+ nop
+ nop
+ nop
+ nop
+ nop
+ nop
+
+.section .text.f2,"ax", at progbits
+f2.cfi:
+ ret
+ nop
+ nop
+ nop
+ nop
+ nop
+ nop
+ nop
+ nop
+ nop
+
+.section .text.f3,"ax", at progbits
+f3.cfi:
+ ret
+ nop
+ nop
+ nop
+ nop
+ nop
+ nop
+ nop
+ nop
+ nop
+
+.section .text.f4,"ax", at progbits
+.type f4.cfi, at function
+f4.cfi:
+ .cfi_startproc
+ ret
+ .cfi_endproc
+.size f4.cfi, .-f4.cfi
\ No newline at end of file
diff --git a/lld/test/ELF/eh-frame-hdr-optimize-bb-jumps.s b/lld/test/ELF/eh-frame-hdr-optimize-bb-jumps.s
new file mode 100644
index 0000000000000..49dae76560c36
--- /dev/null
+++ b/lld/test/ELF/eh-frame-hdr-optimize-bb-jumps.s
@@ -0,0 +1,39 @@
+# REQUIRES: x86
+## Check that .eh_frame_hdr search-table keys match the final PCs of the
+## FDEs they reference after --optimize-bb-jumps.
+
+# RUN: llvm-mc -filetype=obj -triple=x86_64 %s -o %t.o
+# RUN: ld.lld --eh-frame-hdr --optimize-bb-jumps %t.o -o %t
+# RUN: llvm-readelf --unwind %t | FileCheck %s
+
+# CHECK: fde_count: 2
+# CHECK: entry 0 {
+# CHECK-NEXT: initial_location: [[PC0:0x[0-9a-f]+]]
+# CHECK-NEXT: address: [[FDE0:0x[0-9a-f]+]]
+# CHECK: entry 1 {
+# CHECK-NEXT: initial_location: [[PC1:0x[0-9a-f]+]]
+# CHECK-NEXT: address: [[FDE1:0x[0-9a-f]+]]
+# CHECK: [{{ *}}[[FDE0]]{{ *}}] FDE
+# CHECK-NEXT: initial_location: [[PC0]]
+# CHECK: [{{ *}}[[FDE1]]{{ *}}] FDE
+# CHECK-NEXT: initial_location: [[PC1]]
+
+.section .text,"ax", at progbits,unique,1
+.type foo, at function
+foo:
+ nop
+ jmp a.BB.foo
+
+.section .text,"ax", at progbits,unique,2
+.type a.BB.foo, at function
+a.BB.foo:
+ .cfi_startproc
+ nop
+ .cfi_endproc
+
+.section .text,"ax", at progbits,unique,3
+.type r.BB.foo, at function
+r.BB.foo:
+ .cfi_startproc
+ nop
+ .cfi_endproc
More information about the llvm-commits
mailing list