为什么ELF程序headers有两个LOAD入口,而程序布局三段

Why ELF program headers have two LOAD entries, while the program layout three sections

我写了一个 "hello world" 程序并启用了 PIE/PIC。我观察到程序 headers 有 2 个 LOAD 条目:

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
... ...
  LOAD           0x000000 0x00000000 0x00000000 0x00870 0x00870 R E 0x1000
  LOAD           0x000eb0 0x00001eb0 0x00001eb0 0x0015c 0x00164 RW  0x1000

所以在我的理解中,ELF 二进制文件将被加载到 2 个页面中。第一页包含从偏移量 0 到文件大小 0x870 的二进制文件,它是 Read & Execute。由于它们是 0x1000 对齐的,因此第二个条目将加载到第二页,其中包含二进制文件(从偏移量 0xeb00xeb0+0x15c) .此页面具有读写权限。

当我 "pmap" 运行 进程(或 cat /proc/pid/maps)时,它显示 运行 程序有 3 个页面:

b7554000      4K rw---   [ anon ]
b7555000   1700K r-x-- libc-2.19.so
b76fe000      4K ----- libc-2.19.so
b76ff000      8K r---- libc-2.19.so
b7701000      4K rw--- libc-2.19.so
b7702000     12K rw---   [ anon ]
b771b000     16K rw---   [ anon ]
b771f000      4K r-x--   [ anon ]
b7720000    128K r-x-- ld-2.19.so
b7740000      4K r---- ld-2.19.so
b7741000      4K rw--- ld-2.19.so
b7742000      4K r-x-- main
b7743000      4K r---- main
b7744000      4K rw--- main
bfe68000    132K rw---   [ stack ]
 total     2032K

那么 ELF 二进制文件是如何加载的,程序 header 是如何指示 LOAD 指令的?

部分章节 header 如下:

  [13] .text             PROGBITS        00000480 000480 0002c2 00  AX  0   0 16
  [14] .fini             PROGBITS        00000744 000744 000014 00  AX  0   0  4
  [15] .rodata           PROGBITS        00000758 000758 000060 00   A  0   0  4
  [16] .eh_frame         PROGBITS        000007b8 0007b8 000094 00   A  0   0  4
  [17] .eh_frame_hdr     PROGBITS        0000084c 00084c 000024 00   A  0   0  4
  [18] .jcr              PROGBITS        00001eb0 000eb0 000004 00  WA  0   0  4
  [19] .fini_array       FINI_ARRAY      00001eb4 000eb4 000004 00  WA  0   0  4
  [20] .init_array       INIT_ARRAY      00001eb8 000eb8 000004 00  WA  0   0  4
  [21] .dynamic          DYNAMIC         00001ebc 000ebc 000108 08  WA  5   0  4
  [22] .got              PROGBITS        00001fc4 000fc4 00001c 00  WA  0   0  4
  [23] .got.plt          PROGBITS        00001fe0 000fe0 000020 00  WA  0   0  4
  [24] .data             PROGBITS        00002000 001000 00000c 00  WA  0   0  4
  [25] .tm_clone_table   PROGBITS        0000200c 00100c 000000 00  WA  0   0  4
  [26] .bss              NOBITS          0000200c 00100c 000008 00  WA  0   0  4

I observed the program headers have 2 LOAD entries:

您遗漏了一个重要的程序头:GNU_RELRO,它告诉加载器 映射 LOAD 段之后,它应该 mprotect 其中一部分为只读。

当对现有映射调用具有不同权限的 mprotect 时,内核必须将该映射拆分为多个映射,这就是额外条目的来源。

在我的测试二进制文件中:

LOAD           0x000000 0x00000000 0x00000000 0x0075c 0x0075c R E 0x1000
LOAD           0x000ef4 0x00001ef4 0x00001ef4 0x0012c 0x00130 RW  0x1000
...
GNU_RELRO      0x000ef4 0x00001ef4 0x00001ef4 0x0010c 0x0010c R   0x1

内核实际上在加载程序启动之前就映射了 LOAD 个段,此时的映射如下所示:

56555000-56556000 r-xp 00000000 fc:02 801500 /tmp/a.out
56556000-56558000 rw-p 00000000 fc:02 801500 /tmp/a.out
f7fdb000-f7fdc000 r-xp 00000000 00:00 0      [vdso]
...

mprotect之后的地图是这样的:

56555000-56556000 r-xp 00000000 fc:02 801500 /tmp/a.out
56556000-56557000 r--p 00000000 fc:02 801500 /tmp/a.out
56557000-56558000 rw-p 00001000 fc:02 801500 /tmp/a.out
...