在 ELF 文件中跳转 over/removing `PHDR` 程序 header 是否可以执行?如果是这样,为什么?

Is jumping over/removing `PHDR` program header in ELF file for executable OK? If so, why?

我正在对这个简单的 C++ 程序的二进制文件进行一些修改,以理解 ELF 的程序 headers:

int main(){ }

编译为:

❯ make
g++ -O0 -fverbose-asm -no-pie -o main main.cpp

我使用 readelf -l main 得到以下结果:

Elf file type is EXEC (Executable file)
Entry point 0x401020
There are 11 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x0000000000000268 0x0000000000000268  R      0x8
  INTERP         0x00000000000002a8 0x00000000004002a8 0x00000000004002a8
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000004c0 0x00000000000004c0  R      0x1000
...

我在本文档中看到:http://man7.org/linux/man-pages/man5/elf.5.html PHDR:

The array element, if present, specifies the loca‐ tion and size of the program header table itself, both in the file and in the memory image of the pro‐ gram. This segment type may not occur more than once in a file. Moreover, it may occur only if the program header table is part of the memory image of the program. If it is present, it must precede any loadable segment entry.

引用中 if present 的存在让我想知道如果我跳过 PHDR header 会发生什么。 我使用 vim 的十六进制编辑器使用 :%!xxd 更改 main 的二进制布局(在保存之前一定要 运行 :%!xxd -r ,否则它不再是二进制文件)到来自:

00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000  .ELF............
00000010: 0200 3e00 0100 0000 2010 4000 0000 0000  ..>..... .@.....
00000020: 4000 0000 0000 0000 1839 0000 0000 0000  @........9......

至:

00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000  .ELF............
00000010: 0200 3e00 0100 0000 2010 4000 0000 0000  ..>..... .@.....
00000020: 7800 0000 0000 0000 1839 0000 0000 0000  @........9......

(只改变第20个字节),跳过PHDR的长度header。我再次 运行 readelf 验证它仍然是一个有效的 ELF 文件:

❯ readelf -l main

Elf file type is EXEC (Executable file)
Entry point 0x401020
There are 11 program headers, starting at offset 120

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  INTERP         0x00000000000002a8 0x00000000004002a8 0x00000000004002a8
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  ...

而且令人惊讶的是程序仍然执行得很好。为什么我们甚至需要 PHDR header?对链接and/or其他情况有用吗?它似乎在 运行 时间里根本没有使用过,所以为什么我们会把它放在那里?

I run readelf again to verify it's still a valid ELF file:

请注意,尽管 ELF 有效,但它现在已损坏程序头中的第 11 个条目 table(因为您没有减少程序头的数量)。

And surprisingly the program still executes perfectly fine.

此程序不使用动态链接器的任何 功能,因此您破坏其结构的事实不会自行显现。

现在尝试从 libc.so.6 调用一些例程,或者调用 dlopendlsym,看看是否仍然有效。

查看 GLIBC 加载器源代码 (rtld.c),它确实非常关心 PT_PHDR,所以如果没有它仍然可以工作,我会感到惊讶。

如果主程序的类型是 ET_EXEC (non-PIE),它可能在没有 PT_PHDR 的情况下也能运行。 PT_PHDR 的主要用途是能够将 header 中的(未重定位的)地址与程序 headers 的实际运行时地址(由动态链接器通过 AT_PHDR 在 aux 向量中)以确定加载 PIE 可执行文件的偏移量。

我不确定 glibc 的动态链接器对 PT_PHDR 的要求是什么,但在 musl libc 中我们只需要它来计算这个负载偏移量,否则根本不使用它。