[llvm] [llvm-objcopy] Preserve relocated program header tables (PR #220399)

Alexander Wilson via llvm-commits llvm-commits at lists.llvm.org
Tue Sep 1 14:52:04 PDT 2026


https://github.com/rdtscp created https://github.com/llvm/llvm-project/pull/220399

None

>From 860f3262770be761a7e0af9885b593058f96c4a2 Mon Sep 17 00:00:00 2001
From: Alexander Wilson <rdtscp at meta.com>
Date: Tue, 1 Sep 2026 14:42:12 -0700
Subject: [PATCH] [llvm-objcopy] Preserve relocated program header tables

---
 llvm/lib/ObjCopy/ELF/ELFObject.cpp            | 14 ++-
 .../llvm-objcopy/ELF/Inputs/relocated-phdr.py | 95 +++++++++++++++++++
 .../only-keep-debug-relocated-phdr-cover.test | 59 ++++++++++++
 ...nly-keep-debug-relocated-phdr-overlap.test | 53 +++++++++++
 ...y-keep-debug-relocated-phdr-same-load.test | 51 ++++++++++
 .../llvm-objcopy/ELF/only-keep-debug.test     | 60 ++++++++++++
 6 files changed, 329 insertions(+), 3 deletions(-)
 create mode 100644 llvm/test/tools/llvm-objcopy/ELF/Inputs/relocated-phdr.py
 create mode 100644 llvm/test/tools/llvm-objcopy/ELF/only-keep-debug-relocated-phdr-cover.test
 create mode 100644 llvm/test/tools/llvm-objcopy/ELF/only-keep-debug-relocated-phdr-overlap.test
 create mode 100644 llvm/test/tools/llvm-objcopy/ELF/only-keep-debug-relocated-phdr-same-load.test

diff --git a/llvm/lib/ObjCopy/ELF/ELFObject.cpp b/llvm/lib/ObjCopy/ELF/ELFObject.cpp
index ac818343f3f8f..554d2dd410367 100644
--- a/llvm/lib/ObjCopy/ELF/ELFObject.cpp
+++ b/llvm/lib/ObjCopy/ELF/ELFObject.cpp
@@ -2458,8 +2458,12 @@ static uint64_t layoutSegmentsForOnlyKeepDebug(std::vector<Segment *> &Segments,
                                                uint64_t HdrEnd) {
   uint64_t MaxOffset = 0;
   for (Segment *Seg : Segments) {
-    if (Seg->Type == PT_PHDR)
+    if (Seg->Type == PT_PHDR) {
+      // PT_PHDR is preserved at its original offset. Include it in the output
+      // extent even when all section data before it was compacted away.
+      MaxOffset = std::max(MaxOffset, Seg->Offset + Seg->FileSize);
       continue;
+    }
 
     // The segment offset is generally the offset of the first section.
     //
@@ -2519,8 +2523,12 @@ template <class ELFT> void ELFWriter<ELFT>::assignOffsets() {
     // For --only-keep-debug, the sections that did not preserve contents were
     // changed to SHT_NOBITS. We now rewrite sh_offset fields of sections, and
     // then rewrite p_offset/p_filesz of program headers.
-    uint64_t HdrEnd =
-        sizeof(Elf_Ehdr) + llvm::size(Obj.segments()) * sizeof(Elf_Phdr);
+    uint64_t HdrEnd = sizeof(Elf_Ehdr);
+    if (Obj.ProgramHdrSegment.Offset == HdrEnd ||
+        (Obj.ProgramHdrSegment.ParentSegment != nullptr &&
+         Obj.ProgramHdrSegment.ParentSegment ==
+             Obj.ElfHdrSegment.ParentSegment))
+      HdrEnd = Obj.ProgramHdrSegment.Offset + Obj.ProgramHdrSegment.FileSize;
     Offset = layoutSectionsForOnlyKeepDebug(Obj, HdrEnd);
     Offset = std::max(Offset,
                       layoutSegmentsForOnlyKeepDebug(OrderedSegments, HdrEnd));
diff --git a/llvm/test/tools/llvm-objcopy/ELF/Inputs/relocated-phdr.py b/llvm/test/tools/llvm-objcopy/ELF/Inputs/relocated-phdr.py
new file mode 100644
index 0000000000000..cf95443ddbabe
--- /dev/null
+++ b/llvm/test/tools/llvm-objcopy/ELF/Inputs/relocated-phdr.py
@@ -0,0 +1,95 @@
+import struct
+import sys
+from pathlib import Path
+
+
+ELF_PROGRAM_HEADER_SIZE = 56
+ELF_SECTION_HEADER_SIZE = 64
+PT_LOAD = 1
+PT_PHDR = 6
+SHT_NOBITS = 8
+
+
+def read_u16(data, offset):
+    return struct.unpack_from("<H", data, offset)[0]
+
+
+def read_u64(data, offset):
+    return struct.unpack_from("<Q", data, offset)[0]
+
+
+def read_program_headers(data):
+    offset = read_u64(data, 32)
+    entry_size = read_u16(data, 54)
+    count = read_u16(data, 56)
+    assert entry_size == ELF_PROGRAM_HEADER_SIZE
+    end = offset + entry_size * count
+    assert end <= len(data), (
+        f"program header table [{offset:#x}, {end:#x}) exceeds "
+        f"file size {len(data):#x}"
+    )
+    headers = [
+        struct.unpack_from("<IIQQQQQQ", data, offset + i * entry_size)
+        for i in range(count)
+    ]
+    return offset, end, headers
+
+
+def relocate(path, new_offset):
+    path = Path(path)
+    data = bytearray(path.read_bytes())
+    old_offset = read_u64(data, 32)
+    entry_size = read_u16(data, 54)
+    count = read_u16(data, 56)
+    table = data[old_offset : old_offset + entry_size * count]
+    new_end = new_offset + len(table)
+    data.extend(bytes(max(0, new_end - len(data))))
+    data[new_offset:new_end] = table
+    struct.pack_into("<Q", data, 32, new_offset)
+    path.write_bytes(data)
+
+
+def verify(path):
+    data = Path(path).read_bytes()
+    assert data[:4] == b"\x7fELF"
+    assert data[4] == 2, "test helper only supports ELF64"
+    assert data[5] == 1, "test helper only supports little-endian ELF"
+
+    phdr_offset, phdr_end, headers = read_program_headers(data)
+    table_size = phdr_end - phdr_offset
+
+    phdrs = [header for header in headers if header[0] == PT_PHDR]
+    assert len(phdrs) == 1
+    assert phdrs[0][2] == phdr_offset
+    assert phdrs[0][5] == table_size
+
+    loads = [header for header in headers if header[0] == PT_LOAD]
+    assert any(
+        header[2] <= phdr_offset and header[2] + header[5] >= phdr_end
+        for header in loads
+    ), "program header table is not covered by a PT_LOAD"
+
+    section_offset = read_u64(data, 40)
+    section_entry_size = read_u16(data, 58)
+    section_count = read_u16(data, 60)
+    assert section_entry_size == ELF_SECTION_HEADER_SIZE
+    section_end = section_offset + section_entry_size * section_count
+    assert section_end <= len(data)
+
+    for index in range(section_count):
+        header_offset = section_offset + index * section_entry_size
+        section_type = struct.unpack_from("<I", data, header_offset + 4)[0]
+        file_offset = read_u64(data, header_offset + 24)
+        size = read_u64(data, header_offset + 32)
+        if section_type != SHT_NOBITS and size:
+            assert not (
+                file_offset < phdr_end and phdr_offset < file_offset + size
+            ), f"section {index} overlaps the program header table"
+
+
+if sys.argv[1] == "relocate":
+    relocate(sys.argv[2], int(sys.argv[3], 0))
+elif sys.argv[1] == "verify":
+    verify(sys.argv[2])
+else:
+    raise AssertionError(f"unknown operation: {sys.argv[1]}")
diff --git a/llvm/test/tools/llvm-objcopy/ELF/only-keep-debug-relocated-phdr-cover.test b/llvm/test/tools/llvm-objcopy/ELF/only-keep-debug-relocated-phdr-cover.test
new file mode 100644
index 0000000000000..1b428a69a9db0
--- /dev/null
+++ b/llvm/test/tools/llvm-objcopy/ELF/only-keep-debug-relocated-phdr-cover.test
@@ -0,0 +1,59 @@
+# XFAIL: *
+
+## A relocated program header table may be covered by a later PT_LOAD instead
+## of the PT_LOAD containing the ELF header. --only-keep-debug must preserve
+## that coverage, regardless of whether it keeps the table in place or moves
+## the table and its covering PT_LOAD together.
+##
+## FIXME: Remove XFAIL once the relocated program header table is treated as
+## an occupied range during --only-keep-debug layout.
+
+# RUN: yaml2obj %s -o %t
+# RUN: %python %S/Inputs/relocated-phdr.py relocate %t 0x1000
+# RUN: %python %S/Inputs/relocated-phdr.py verify %t
+# RUN: llvm-objcopy --only-keep-debug %t %t.dbg
+# RUN: %python %S/Inputs/relocated-phdr.py verify %t.dbg
+# RUN: llvm-readelf -x .note %t.dbg | FileCheck %s
+
+# CHECK:      Hex dump of section '.note':
+# CHECK-NEXT: 0x00000100 04000000 04000000 01000000 474e5500
+# CHECK-NEXT: 0x00000110 01020304
+
+--- !ELF
+FileHeader:
+  Class:   ELFCLASS64
+  Data:    ELFDATA2LSB
+  Type:    ET_EXEC
+  Machine: EM_X86_64
+Sections:
+  - Name:         .note
+    Type:         SHT_NOTE
+    Flags:        [ SHF_ALLOC ]
+    Address:      0x100
+    Offset:       0x100
+    Content:      040000000400000001000000474e550001020304
+  - Name:    .debug_info
+    Type:    SHT_PROGBITS
+    Offset:  0x2000
+    Content: '00'
+ProgramHeaders:
+  - Type:     PT_PHDR
+    Flags:    [ PF_R ]
+    Offset:   0x1000
+    VAddr:    0x1000
+    FileSize: 0xa8
+    MemSize:  0xa8
+    Align:    8
+  - Type:     PT_LOAD
+    Flags:    [ PF_R ]
+    Offset:   0
+    Align:    0x1000
+    FirstSec: .note
+    LastSec:  .note
+  - Type:     PT_LOAD
+    Flags:    [ PF_R ]
+    Offset:   0x1000
+    VAddr:    0x1000
+    FileSize: 0xa8
+    MemSize:  0xa8
+    Align:    0x1000
diff --git a/llvm/test/tools/llvm-objcopy/ELF/only-keep-debug-relocated-phdr-overlap.test b/llvm/test/tools/llvm-objcopy/ELF/only-keep-debug-relocated-phdr-overlap.test
new file mode 100644
index 0000000000000..d8a08eb49732d
--- /dev/null
+++ b/llvm/test/tools/llvm-objcopy/ELF/only-keep-debug-relocated-phdr-overlap.test
@@ -0,0 +1,53 @@
+# XFAIL: *
+
+## Retained section data must not be compacted across a relocated program
+## header table. llvm-objcopy writes standalone section contents after the
+## program headers, so an overlap would silently replace the table.
+##
+## FIXME: Remove XFAIL once the relocated program header table is treated as
+## an occupied range during --only-keep-debug layout.
+
+# RUN: yaml2obj %s -o %t
+# RUN: %python %S/Inputs/relocated-phdr.py relocate %t 0x1000
+# RUN: %python %S/Inputs/relocated-phdr.py verify %t
+# RUN: llvm-objcopy --only-keep-debug %t %t.dbg
+# RUN: %python %S/Inputs/relocated-phdr.py verify %t.dbg
+
+--- !ELF
+FileHeader:
+  Class:   ELFCLASS64
+  Data:    ELFDATA2LSB
+  Type:    ET_EXEC
+  Machine: EM_X86_64
+Sections:
+  - Name:         .note
+    Type:         SHT_NOTE
+    Flags:        [ SHF_ALLOC ]
+    Address:      0x100
+    Offset:       0x100
+    Content:      040000000400000001000000474e550001020304
+  - Name:    .debug_info
+    Type:    SHT_PROGBITS
+    Offset:  0x2000
+    Size:    0x1000
+ProgramHeaders:
+  - Type:     PT_PHDR
+    Flags:    [ PF_R ]
+    Offset:   0x1000
+    VAddr:    0x1000
+    FileSize: 0xa8
+    MemSize:  0xa8
+    Align:    8
+  - Type:     PT_LOAD
+    Flags:    [ PF_R ]
+    Offset:   0
+    Align:    0x1000
+    FirstSec: .note
+    LastSec:  .note
+  - Type:     PT_LOAD
+    Flags:    [ PF_R ]
+    Offset:   0x1000
+    VAddr:    0x1000
+    FileSize: 0xa8
+    MemSize:  0xa8
+    Align:    0x1000
diff --git a/llvm/test/tools/llvm-objcopy/ELF/only-keep-debug-relocated-phdr-same-load.test b/llvm/test/tools/llvm-objcopy/ELF/only-keep-debug-relocated-phdr-same-load.test
new file mode 100644
index 0000000000000..aa81613dfad82
--- /dev/null
+++ b/llvm/test/tools/llvm-objcopy/ELF/only-keep-debug-relocated-phdr-same-load.test
@@ -0,0 +1,51 @@
+# XFAIL: *
+
+## A relocated program header table can share a PT_LOAD with the ELF header
+## without immediately following it. The table must not make earlier retained
+## section contents move relative to their segment data.
+##
+## FIXME: Remove XFAIL once the relocated program header table is treated as
+## an occupied range during --only-keep-debug layout.
+
+# RUN: yaml2obj %s -o %t
+# RUN: %python %S/Inputs/relocated-phdr.py relocate %t 0x1000
+# RUN: %python %S/Inputs/relocated-phdr.py verify %t
+# RUN: llvm-objcopy --only-keep-debug %t %t.dbg
+# RUN: %python %S/Inputs/relocated-phdr.py verify %t.dbg
+# RUN: llvm-readelf -x .note %t.dbg | FileCheck %s
+
+# CHECK:      Hex dump of section '.note':
+# CHECK-NEXT: 0x00000100 04000000 04000000 01000000 474e5500
+# CHECK-NEXT: 0x00000110 01020304
+
+--- !ELF
+FileHeader:
+  Class:   ELFCLASS64
+  Data:    ELFDATA2LSB
+  Type:    ET_EXEC
+  Machine: EM_X86_64
+Sections:
+  - Name:         .note
+    Type:         SHT_NOTE
+    Flags:        [ SHF_ALLOC ]
+    Address:      0x100
+    Offset:       0x100
+    Content:      040000000400000001000000474e550001020304
+  - Name:    .debug_info
+    Type:    SHT_PROGBITS
+    Offset:  0x2000
+    Content: '00'
+ProgramHeaders:
+  - Type:     PT_PHDR
+    Flags:    [ PF_R ]
+    Offset:   0x1000
+    VAddr:    0x1000
+    FileSize: 0x70
+    MemSize:  0x70
+    Align:    8
+  - Type:     PT_LOAD
+    Flags:    [ PF_R ]
+    Offset:   0
+    FileSize: 0x1070
+    MemSize:  0x1070
+    Align:    0x1000
diff --git a/llvm/test/tools/llvm-objcopy/ELF/only-keep-debug.test b/llvm/test/tools/llvm-objcopy/ELF/only-keep-debug.test
index c144b1d35c043..3068ff3d75e24 100644
--- a/llvm/test/tools/llvm-objcopy/ELF/only-keep-debug.test
+++ b/llvm/test/tools/llvm-objcopy/ELF/only-keep-debug.test
@@ -395,3 +395,63 @@ Sections:
       - Name:            qux
       - Name:            .shstrtab
       - Name:            .strtab
+
+## Check that a program header table located after compacted section data is
+## retained in the output.
+# RUN: yaml2obj --docnum=7 %s -o %t7
+# RUN: %python -c "import struct; p = '%/t7'; d = bytearray(open(p, 'rb').read()); n = struct.unpack_from('<H', d, 56)[0]; z = struct.unpack_from('<H', d, 54)[0]; h = d[64:64+n*z]; d.extend(bytes(max(0, 0x1000 + len(h) - len(d)))); d[0x1000:0x1000+len(h)] = h; struct.pack_into('<Q', d, 32, 0x1000); open(p, 'wb').write(d)"
+# RUN: llvm-objcopy --only-keep-debug %t7 %t7.dbg
+# RUN: llvm-readobj --file-headers --program-headers --sections %t7.dbg | FileCheck --check-prefix=CHECK7 %s
+# RUN: llvm-readelf -x .note %t7.dbg | FileCheck --check-prefix=CHECK7-NOTE %s
+
+# CHECK7:      ProgramHeaderOffset: 0x1000
+# CHECK7:      SectionHeaderOffset: 0x10A8
+# CHECK7:      ProgramHeaderCount: 3
+# CHECK7:      Name: .note
+# CHECK7:      Offset: 0x100
+# CHECK7:      ProgramHeaders [
+# CHECK7:        ProgramHeader {
+# CHECK7-NEXT:     Type: PT_PHDR
+# CHECK7-NEXT:     Offset: 0x1000
+
+# CHECK7-NOTE: Hex dump of section '.note':
+# CHECK7-NOTE: 0x00000100 04000000 04000000 01000000 474e5500
+# CHECK7-NOTE: 0x00000110 01020304
+
+--- !ELF
+FileHeader:
+  Class:   ELFCLASS64
+  Data:    ELFDATA2LSB
+  Type:    ET_EXEC
+  Machine: EM_X86_64
+Sections:
+  - Name:         .note
+    Type:         SHT_NOTE
+    Flags:        [ SHF_ALLOC ]
+    Address:      0x100
+    Offset:       0x100
+    Content:      040000000400000001000000474e550001020304
+  - Name:    .debug_info
+    Type:    SHT_PROGBITS
+    Content: '00'
+ProgramHeaders:
+  - Type:     PT_PHDR
+    Flags:    [ PF_R ]
+    Offset:   0x1000
+    VAddr:    0x1000
+    FileSize: 0xa8
+    MemSize:  0xa8
+    Align:    8
+  - Type:     PT_LOAD
+    Flags:    [ PF_R ]
+    Offset:   0
+    Align:    0x1000
+    FirstSec: .note
+    LastSec:  .note
+  - Type:     PT_LOAD
+    Flags:    [ PF_R ]
+    Offset:   0x1000
+    VAddr:    0x1000
+    FileSize: 0xa8
+    MemSize:  0xa8
+    Align:    0x1000



More information about the llvm-commits mailing list