我正在尝试将任意数据嵌入到 ELF 可执行文件中,并让 Linux 在加载时自动映射它。最近询问了关于此的另一个问题,最终支持将此用例添加到
mold
链接器中。
我编写了一个工具,可以在可执行文件末尾附加任意数据,并在指向附加数据的
PT_LOAD
ELF 程序头中进行修补。这是修补逻辑:
appended_data_file_offset = /* ... seek(elf file, SEEK_END) ... */;
appended_data_size = /* ... stat(data file) ... */;
phdr->p_type = PT_LOAD;
phdr->p_filesz = phdr->p_memsz = appended_data_size;
size_t base = phdr->p_vaddr - phdr->p_offset; // calculate program's base load address
phdr->p_vaddr = phdr->p_paddr = base + appended_data_file_offset;
phdr->p_offset = appended_data_file_offset;
phdr->p_align = 1;
phdr->p_flags = PF_R;
运行我的修补程序会生成一个 ELF 文件,并在偏移量处附加此数据
0xAD78
:
0000ad70: 00 00 00 00 00 00 00 00 74 65 73 74 20 64 61 74 ........test dat
0000ad80: 61 0a a.
添加了这个
PT_LOAD
段:
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x000000000000ad78 0x000000000020ad78 0x000000000020ad78
0x000000000000000a 0x000000000000000a R 0x1
这个新段和末尾的 10 字节块是对完美运行的 ELF 可执行文件所做的唯一更改。通过二进制比较确认。
在运行时,程序应该达到该数据。它通过辅助向量来实现:
Elf64_Phdr *header = (Elf64_Phdr *) getauxval(AT_PHDR);
size_t count = getauxval(AT_PHNUM);
size_t size = getauxval(AT_PHENT);
assert(size == sizeof(Elf64_Phdr));
for (size_t i = 0; i < count; ++header, ++i) {
if (header->p_type != PT_LOAD) { continue; }
if (0 == memcmp(header->p_vaddr, "test", sizeof("test") - 1)) {
// found it
}
}
为了清楚起见,我使用了
libc
函数。我的实际程序是一个用独立 C 编写的静态 EXEC
ELF 文件。它不链接到 libc
并直接使用 Linux 系统调用。
以这种方式修补可执行文件后,我打算发生这种情况:
0xAD78
的 10 字节块。AT_PHDR
值找到程序头表。PT_LOAD
段,直到找到数据。
p_vaddr
应指向包含 "test data\n"
相反,这个程序完全崩溃了。不执行任何一条指令。甚至没有到达入口点。连
gdb
都无法调试它:
(gdb) run
Starting program: exe.patched
During startup program terminated with signal SIGSEGV, Segmentation fault.
(gdb) info registers
The program has no registers now.
(gdb) step
The program is not being run.
尽管没有
PT_LOAD
标头,它运行没有任何问题。如果我将类型更改为 PT_LOOS
或任何其他类型,它也有效。
我想不通。我到底做错了什么?
完整的
readelf
打印输出:
$ readelf --file-header --program-headers program.patched
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: AArch64
Version: 0x1
Entry point address: 0x2037d8
Start of program headers: 64 (bytes into file)
Start of section headers: 43512 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 5
Size of section headers: 64 (bytes)
Number of section headers: 8
Section header string table index: 6
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x000000000000abf8 0x000000000020abf8 0x000000000020abf8
0x000000000000000a 0x000000000000000a R 0x1
LOAD 0x0000000000000000 0x0000000000200000 0x0000000000200000
0x00000000000027d8 0x00000000000027d8 R 0x1000
LOAD 0x00000000000027d8 0x00000000002037d8 0x00000000002037d8
0x0000000000005ed8 0x0000000000005ed8 R E 0x1000
LOAD 0x00000000000086b0 0x000000000020a6b0 0x000000000020a6b0
0x0000000000000000 0x0000000000100015 RW 0x1000
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x0
Section to Segment mapping:
Segment Sections...
00
01 .rodata
02 .text
03 .bss
04
PT_LOAD 标头必须按虚拟地址的升序排列。您的新程序标头具有比以下所有 PT_LOAD 标头更高的
p_vaddr
。
此外,该段的虚拟地址范围不应重叠,但您的新段位于最后一个段内。映射段的相关大小是
p_filesz
和 p_memsz
中的较大者。
这记录在 man 5 elf中。