[lld] d53bf41 - [ELF] Support DW_EH_PE_sdata8 encoding in .eh_frame_hdr (#179089)
via llvm-commits
llvm-commits at lists.llvm.org
Tue Feb 3 19:58:57 PST 2026
Author: Fangrui Song
Date: 2026-02-03T19:58:52-08:00
New Revision: d53bf41395873918d0759f5ed17876143c64a635
URL: https://github.com/llvm/llvm-project/commit/d53bf41395873918d0759f5ed17876143c64a635
DIFF: https://github.com/llvm/llvm-project/commit/d53bf41395873918d0759f5ed17876143c64a635.diff
LOG: [ELF] Support DW_EH_PE_sdata8 encoding in .eh_frame_hdr (#179089)
Currently, both GNU ld and lld only support
`table_enc = DW_EH_PE_datarel | DW_EH_PE_sdata4` for .eh_frame_hdr.
When a table entry exceeds the 32-bit range, we can use
`DW_EH_PE_sdata8` instead of reporting an error
(https://reviews.llvm.org/D49607 introduced the "PC offset is too large"
error). This is useful for certain large executables.
This patch auto-detects when 64-bit encoding is needed and upgrades
the encoding accordingly. We use DW_EH_PE_sdata8 when either a table
entry or eh_frame_ptr exceeds the 32-bit range.
Technically, eh_frame_ptr could remain sdata4 when only table entries
require sdata8, but there is little value in this flexibility since
.eh_frame/.eh_frame_hdr distance is a much less strict constraint than
.text/.eh_frame_hdr distance.
The implementation caches FDE data in EhFrameHeader during
updateAllocSize and reuses it in writeTo.
Previously, `EhFrameSection::getFdePc` was used to read the
`initial_location` value from .eh_frame FDEs. Since the value equals the
relocated symbol's VA for all cases that matter, we now obtain the value
directly and simplify the implementation.
Also update the outdated comment about sorting the FDE list; the ICF
case mentioned there no longer applies after
https://reviews.llvm.org/D116093.
In the new test eh-frame-hdr-sdata8.s, 3.lds is carefully crafted to
ensure that .eh_frame_hdr size requires fixed-point iteration (to test
`updateAllocSize`). This is also groundwork for compact unwind support
similar to Mach-O `__unwind_info`.
Close #172777
Added:
lld/test/ELF/eh-frame-hdr-sdata8.s
Modified:
lld/ELF/SyntheticSections.cpp
lld/ELF/SyntheticSections.h
lld/ELF/Writer.cpp
Removed:
lld/test/ELF/eh-frame-pcrel-overflow.s
################################################################################
diff --git a/lld/ELF/SyntheticSections.cpp b/lld/ELF/SyntheticSections.cpp
index d7b1071eb7075..2cfc88d8389b0 100644
--- a/lld/ELF/SyntheticSections.cpp
+++ b/lld/ELF/SyntheticSections.cpp
@@ -540,43 +540,6 @@ void EhFrameSection::finalizeContents() {
this->size = off;
}
-static uint64_t readFdeAddr(Ctx &ctx, uint8_t *buf, int size) {
- switch (size) {
- case DW_EH_PE_udata2:
- return read16(ctx, buf);
- case DW_EH_PE_sdata2:
- return (int16_t)read16(ctx, buf);
- case DW_EH_PE_udata4:
- return read32(ctx, buf);
- case DW_EH_PE_sdata4:
- return (int32_t)read32(ctx, buf);
- case DW_EH_PE_udata8:
- case DW_EH_PE_sdata8:
- return read64(ctx, buf);
- case DW_EH_PE_absptr:
- return readUint(ctx, buf);
- }
- Err(ctx) << "unknown FDE size encoding";
- return 0;
-}
-
-// Returns the VA to which a given FDE (on a mmap'ed buffer) is applied to.
-// We need it to create .eh_frame_hdr section.
-uint64_t EhFrameSection::getFdePc(uint8_t *buf, size_t fdeOff,
- uint8_t enc) const {
- // The starting address to which this FDE applies is
- // stored at FDE + 8 byte. And this offset is within
- // the .eh_frame section.
- size_t off = fdeOff + 8;
- uint64_t addr = readFdeAddr(ctx, buf + off, enc & 0xf);
- if ((enc & 0x70) == DW_EH_PE_absptr)
- return ctx.arg.is64 ? addr : uint32_t(addr);
- if ((enc & 0x70) == DW_EH_PE_pcrel)
- return addr + getParent()->addr + off + outSecOff;
- Err(ctx) << "unknown FDE size relative encoding";
- return 0;
-}
-
void EhFrameSection::writeTo(uint8_t *buf) {
// Write CIE and FDE records.
for (CieRecord *rec : cieRecords) {
@@ -602,53 +565,31 @@ void EhFrameSection::writeTo(uint8_t *buf) {
if (!hdr || !hdr->getParent())
return;
- // Write the .eh_frame_hdr section, which contains a binary search table of
- // pointers to FDEs. This must be written after .eh_frame relocation since
- // the content depends on relocated initial_location fields in FDEs.
- using FdeData = EhFrameSection::FdeData;
- SmallVector<FdeData, 0> fdes;
- uint64_t va = hdr->getVA();
- for (CieRecord *rec : cieRecords) {
- uint8_t enc = getFdeEncoding(rec->cie);
- for (EhSectionPiece *fde : rec->fdes) {
- uint64_t pc = getFdePc(buf, fde->outputOff, enc);
- uint64_t fdeVA = getParent()->addr + fde->outputOff;
- if (!isInt<32>(pc - va)) {
- Err(ctx) << fde->sec << ": PC offset is too large: 0x"
- << Twine::utohexstr(pc - va);
- continue;
- }
- fdes.push_back({uint32_t(pc - va), uint32_t(fdeVA - va)});
- }
- }
-
- // Sort the FDE list by their PC and uniqueify. Usually there is only
- // one FDE for a PC (i.e. function), but if ICF merges two functions
- // into one, there can be more than one FDEs pointing to the address.
- llvm::stable_sort(fdes, [](const FdeData &a, const FdeData &b) {
- return a.pcRel < b.pcRel;
- });
- fdes.erase(
- llvm::unique(fdes, [](auto &a, auto &b) { return a.pcRel == b.pcRel; }),
- fdes.end());
+ // Write the .eh_frame_hdr section using cached FDE data from updateAllocSize.
+ bool large = hdr->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);
+ };
- // Write header.
uint8_t *hdrBuf = ctx.bufferStart + hdr->getParent()->offset + hdr->outSecOff;
- hdrBuf[0] = 1; // version
- hdrBuf[1] = DW_EH_PE_pcrel | DW_EH_PE_sdata4; // eh_frame_ptr_enc
- hdrBuf[2] = DW_EH_PE_udata4; // fde_count_enc
- hdrBuf[3] = DW_EH_PE_datarel | DW_EH_PE_sdata4; // table_enc
- write32(ctx, hdrBuf + 4,
- getParent()->addr - hdr->getVA() - 4); // eh_frame_ptr
- write32(ctx, hdrBuf + 8, fdes.size()); // fde_count
- hdrBuf += 12;
-
- // Write binary search table. Each entry describes the starting PC and the FDE
- // address.
- for (FdeData &fde : fdes) {
- write32(ctx, hdrBuf, fde.pcRel);
- write32(ctx, hdrBuf + 4, fde.fdeVARel);
- hdrBuf += 8;
+ // version
+ hdrBuf[0] = 1;
+ // eh_frame_ptr_enc
+ hdrBuf[1] = DW_EH_PE_pcrel | (large ? DW_EH_PE_sdata8 : DW_EH_PE_sdata4);
+ // fde_count_enc
+ hdrBuf[2] = DW_EH_PE_udata4;
+ // table_enc
+ hdrBuf[3] = DW_EH_PE_datarel | (large ? DW_EH_PE_sdata8 : DW_EH_PE_sdata4);
+ hdrBuf += 4;
+ writeField(hdrBuf, ehFramePtr);
+ hdrBuf += large ? 8 : 4;
+ write32(ctx, hdrBuf, hdr->fdes.size());
+ hdrBuf += 4;
+ for (const FdeData &fde : hdr->fdes) {
+ writeField(hdrBuf, fde.pcRel);
+ writeField(hdrBuf + (large ? 8 : 4), fde.fdeVARel);
+ hdrBuf += large ? 16 : 8;
}
}
@@ -659,15 +600,71 @@ void EhFrameHeader::writeTo(uint8_t *buf) {
// The section content is written during EhFrameSection::writeTo.
}
-size_t EhFrameHeader::getSize() const {
- // .eh_frame_hdr has a 12 bytes header followed by an array of FDEs.
- return 12 + getPartition(ctx).ehFrame->numFdes * 8;
-}
-
bool EhFrameHeader::isNeeded() const {
return isLive() && getPartition(ctx).ehFrame->isNeeded();
}
+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 = getPartition(ctx).ehFrame->numFdes;
+ size = 4 + (large ? 8 : 4) + 4 + numFdes * (large ? 16 : 8);
+}
+
+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.
+ EhFrameSection *ehFrame = getPartition(ctx).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);
+
+ // 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) {
+ Err(ctx) << "unknown FDE size encoding";
+ continue;
+ }
+ for (EhSectionPiece *fde : rec->fdes) {
+ // The FDE has passed `isFdeLive`, so the first relocation's symbol is a
+ // live Defined.
+ auto *isec = cast<EhInputSection>(fde->sec);
+ auto &reloc = isec->rels[fde->firstRelocation];
+ 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);
+ }
+ }
+
+ // 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.
+ size_t oldSize = size;
+ finalizeContents();
+ return size != oldSize;
+}
+
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 cad3779ee9675..64f0a477e2c23 100644
--- a/lld/ELF/SyntheticSections.h
+++ b/lld/ELF/SyntheticSections.h
@@ -64,8 +64,8 @@ class EhFrameSection final : public SyntheticSection {
size_t numFdes = 0;
struct FdeData {
- uint32_t pcRel;
- uint32_t fdeVARel;
+ int64_t pcRel;
+ int64_t fdeVARel;
};
ArrayRef<CieRecord *> getCieRecords() const { return cieRecords; }
@@ -86,8 +86,6 @@ class EhFrameSection final : public SyntheticSection {
CieRecord *addCie(EhSectionPiece &piece, ArrayRef<Relocation> rels);
Defined *isFdeLive(EhSectionPiece &piece, ArrayRef<Relocation> rels);
- uint64_t getFdePc(uint8_t *buf, size_t off, uint8_t enc) const;
-
SmallVector<CieRecord *, 0> cieRecords;
// CIE records are uniquified by their contents and personality functions.
@@ -101,8 +99,16 @@ class EhFrameHeader final : public SyntheticSection {
public:
EhFrameHeader(Ctx &);
void writeTo(uint8_t *buf) override;
- size_t getSize() const override;
+ size_t getSize() const override { return size; }
bool isNeeded() const override;
+ 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.
+ size_t size = 0;
};
class GotSection final : public SyntheticSection {
diff --git a/lld/ELF/Writer.cpp b/lld/ELF/Writer.cpp
index 17d2a77493d19..9220d73559b0b 100644
--- a/lld/ELF/Writer.cpp
+++ b/lld/ELF/Writer.cpp
@@ -1603,6 +1603,8 @@ template <class ELFT> void Writer<ELFT>::finalizeAddressDependentContent() {
changed |= part.relrAuthDyn->updateAllocSize(ctx);
if (part.memtagGlobalDescriptors)
changed |= part.memtagGlobalDescriptors->updateAllocSize(ctx);
+ if (part.ehFrameHdr && part.ehFrameHdr->isNeeded())
+ changed |= part.ehFrameHdr->updateAllocSize(ctx);
}
std::pair<const OutputSection *, const Defined *> changes =
@@ -1627,6 +1629,10 @@ template <class ELFT> void Writer<ELFT>::finalizeAddressDependentContent() {
// Spilling can change relative section order.
finalizeOrderDependentContent();
}
+ // If updateAllocSize reported errors (e.g. "unknown FDE size encoding" for
+ // part.ehFrameHdr), break to avoid duplicate diagnostics from the loop.
+ if (errCount(ctx))
+ break;
}
if (!ctx.arg.relocatable)
ctx.target->finalizeRelax(pass);
diff --git a/lld/test/ELF/eh-frame-hdr-sdata8.s b/lld/test/ELF/eh-frame-hdr-sdata8.s
new file mode 100644
index 0000000000000..f249e2418282c
--- /dev/null
+++ b/lld/test/ELF/eh-frame-hdr-sdata8.s
@@ -0,0 +1,110 @@
+# REQUIRES: x86
+
+## Test that .eh_frame_hdr uses DW_EH_PE_sdata8 instead of DW_EH_PE_sdata4 when
+## eh_frame_ptr or a table entry exceeds the 32-bit range.
+
+# RUN: rm -rf %t && split-file %s %t && cd %t
+# RUN: llvm-mc -filetype=obj -triple=x86_64 a.s --large-code-model -o a.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64 bad.s -o bad.o
+
+## Case 1: .text at high address - pcRel exceeds 32-bit range.
+# RUN: ld.lld --eh-frame-hdr -T 1.lds a.o -o out1
+# RUN: llvm-objdump -s -j .eh_frame_hdr out1 | FileCheck %s --check-prefix=CHECK1
+
+## Case 2: .eh_frame at high address - eh_frame_ptr exceeds 32-bit range.
+# RUN: ld.lld --eh-frame-hdr -T 2.lds a.o -o out2
+# RUN: llvm-objdump -s -j .eh_frame_hdr out2 | FileCheck %s --check-prefix=CHECK2
+
+## Case 3: .eh_frame_hdr and .relr.dyn sizes are coupled, requiring multiple iterations
+## to stabilize.
+# RUN: ld.lld -pie --eh-frame-hdr -z pack-relative-relocs -T 3.lds a.o -o out3
+# RUN: llvm-objdump -s -j .eh_frame_hdr out3 | FileCheck %s --check-prefix=CHECK3
+
+## Header: version=1, eh_frame_ptr_enc=0x1C (pcrel|sdata8),
+## fde_count_enc=0x03 (udata4), table_enc=0x3C (datarel|sdata8)
+## Layout: header (4) + eh_frame_ptr (8) + fde_count (4) + table entries (16 each)
+## Each table entry: pcRel (8) + fdeVARel (8), relative to .eh_frame_hdr address
+# CHECK1: section .eh_frame_hdr:
+# CHECK1-NEXT: 011c033c 2c000000 00000000 02000000
+# CHECK1-NEXT: 00100000 01000000 48000000 00000000
+# CHECK1-NEXT: 01100000 01000000 68000000 00000000
+# CHECK1-EMPTY:
+# CHECK2: section .eh_frame_hdr:
+# CHECK2-NEXT: 011c033c f80f0000 01000000 02000000
+# CHECK2-NEXT: fcffffff ffffffff 14100000 01000000
+# CHECK2-NEXT: fdffffff ffffffff 34100000 01000000
+# CHECK2-EMPTY:
+# CHECK3: 011c033c 2c000000 00000000 02000000
+# CHECK3-NEXT: 00800000 01000000 48000000 00000000
+
+## A corrupted .eh_frame reports exactly one error (not duplicated by the loop).
+# RUN: not ld.lld --eh-frame-hdr -T 1.lds a.o bad.o 2>&1 | FileCheck %s --check-prefix=ERR --implicit-check-not=error:
+# ERR: error: corrupted .eh_frame: unexpected end of CIE
+
+#--- a.s
+.text
+.global _start
+_start:
+ .cfi_startproc
+ nop
+ .cfi_endproc
+ .cfi_startproc
+ nop
+ .cfi_endproc
+
+.data
+.balign 8
+## Two adjacent relocations use 2 RELR entries (1 address + 1 bitmap).
+.dc.a __ehdr_start
+.dc.a __ehdr_start
+
+.section .data.1,"aw"
+.balign 8
+## A RELR bitmap entry can encode up to 63 relocations with word-sized stride.
+## If .data.1 is >= 63*8 bytes from end(.data), this relocation cannot reuse
+## the previous bitmap entry, requiring a third RELR entry.
+.dc.a __ehdr_start
+
+#--- 1.lds
+SECTIONS {
+ . = 0x1000;
+ .eh_frame_hdr : {}
+ .eh_frame : {}
+ .text 0x100002000 : {}
+}
+
+#--- 2.lds
+SECTIONS {
+ . = 0x1000;
+ .text : {}
+ .eh_frame_hdr : {}
+ .eh_frame 0x100002000 : {}
+}
+
+#--- 3.lds
+SECTIONS {
+ ## Test that .eh_frame_hdr and .relr.dyn sizes are coupled, requiring
+ ## multiple finalizeAddressDependentContent iterations to converge.
+ ##
+ ## The padding before .data.1 is set so that switching .eh_frame_hdr from
+ ## sdata4 (18 bytes) to sdata8 (48 bytes) pushes .data.1 past the 63*8-byte
+ ## RELR bitmap threshold, growing .relr.dyn from 16 to 24 bytes.
+ ## The .text address depends on SIZEOF(.relr.dyn), creating the coupling.
+ .eh_frame_hdr : {}
+ .eh_frame : {}
+ .relr.dyn : {}
+ .data : { *(.data) . += 63*8-40 + SIZEOF(.eh_frame_hdr); *(.data.*) }
+ . = SIZEOF(.relr.dyn) > 16 ? 0x100008000 : 0x3000;
+ .text : {}
+ ASSERT(SIZEOF(.relr.dyn) > 16, ".relr.dyn size should increase from 16 to 24")
+}
+
+#--- bad.s
+## Malformed CIE: length says 8 bytes but content is truncated.
+.section .eh_frame,"a", at unwind
+ .long 8 # length
+ .long 0 # CIE id
+ .byte 1 # version
+ .byte 0 # augmentation string (empty)
+ ## Missing: code/data alignment, return column, etc.
+ .space 2
diff --git a/lld/test/ELF/eh-frame-pcrel-overflow.s b/lld/test/ELF/eh-frame-pcrel-overflow.s
deleted file mode 100644
index 3dfcf9ee1a7f9..0000000000000
--- a/lld/test/ELF/eh-frame-pcrel-overflow.s
+++ /dev/null
@@ -1,35 +0,0 @@
-# REQUIRES: x86
-
-# RUN: llvm-mc -filetype=obj -triple=x86_64-pc-linux %s -o %t.o
-# RUN: llvm-mc -filetype=obj -triple=x86_64-pc-linux %p/Inputs/eh-frame-pcrel-overflow.s -o %t1.o
-# RUN: ld.lld --eh-frame-hdr -Ttext=0x90000000 %t.o -o /dev/null
-# RUN: not ld.lld --eh-frame-hdr %t.o %t1.o -o /dev/null 2>&1 | FileCheck %s
-# RUN: ld.lld --eh-frame-hdr %t.o %t1.o -o /dev/null --noinhibit-exec 2>&1 | FileCheck %s --check-prefix=WARN
-# CHECK: error: {{.*}}.o:(.eh_frame): PC offset is too large: 0x90001054
-# WARN: warning: {{.*}}.o:(.eh_frame): PC offset is too large: 0x90001054
-
-.text
-.global _start
-_start:
- ret
-
-.section .eh_frame,"a", at unwind
- .long 12 # Size
- .long 0x00 # ID
- .byte 0x01 # Version.
-
- .byte 0x52 # Augmentation string: 'R','\0'
- .byte 0x00
-
- .byte 0x01
-
- .byte 0x01 # LEB128
- .byte 0x01 # LEB128
-
- .byte 0x00 # DW_EH_PE_absptr
-
- .byte 0xFF
-
- .long 12 # Size
- .long 0x14 # ID
- .quad _start + 0x70000000
More information about the llvm-commits
mailing list