[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