一个 ELF 可执行文件为什么会有 4 个 LOAD 段?

10

有一台远程的64位*nix服务器,可以编译用户提供的代码(代码应该是用Rust编写的,但我认为这不重要,因为它使用LLVM)。我不知道它使用哪些编译器/链接器标志,但编译后的ELF可执行文件看起来很奇怪——它有4个LOAD段:

$ readelf -e executable
...
Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
...
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000004138 0x0000000000004138  R      0x1000
  LOAD           0x0000000000005000 0x0000000000005000 0x0000000000005000
                 0x00000000000305e9 0x00000000000305e9  R E    0x1000
  LOAD           0x0000000000036000 0x0000000000036000 0x0000000000036000
                 0x000000000000d808 0x000000000000d808  R      0x1000
  LOAD           0x0000000000043da0 0x0000000000044da0 0x0000000000044da0
                 0x0000000000002290 0x00000000000024a0  RW     0x1000
...


在我的系统上,我查看的所有可执行文件都只有两个LOAD段。
Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
...
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x00000000003000c0 0x00000000003000c0  R E    0x200000
  LOAD           0x00000000003002b0 0x00000000005002b0 0x00000000005002b0
                 0x00000000000776c8 0x000000000009b200  RW     0x200000
...
  1. 在什么情况下(编译器/链接器版本、标志等)编译器可能会构建一个有4个LOAD段的ELF文件?
  2. 有4个LOAD段的意义是什么?我想,拥有一个读取但无法执行权限的段可能有助于防止某些攻击,但为什么要有两个这样的段?

找到了部分答案:lld默认链接3个段(R、R+E和R+W)。 - kreo
GNU Binutils的ld也可以用于C程序。 - Peter Cordes
1个回答

20

一个典型的使用 BFD-ld 或 Gold 连接的 Linux 可执行文件具有 2 个可加载的段,其中 ELF 头与 .text 和 .rodata 合并到第一个 RE 段中,并且 .data、.bss 和其他可写节合并到第二个 RW 段中。

以下是典型的节到段映射:

$ echo "int foo; int main() { return 0;}"  | clang -xc - -o a.out-gold -fuse-ld=gold
$ readelf -Wl a.out-gold

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

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  PHDR           0x000040 0x0000000000400040 0x0000000000400040 0x0001f8 0x0001f8 R   0x8
  INTERP         0x000238 0x0000000000400238 0x0000000000400238 0x00001c 0x00001c R   0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x000000 0x0000000000400000 0x0000000000400000 0x0006b0 0x0006b0 R E 0x1000
  LOAD           0x000e18 0x0000000000401e18 0x0000000000401e18 0x0001f8 0x000200 RW  0x1000
  DYNAMIC        0x000e28 0x0000000000401e28 0x0000000000401e28 0x0001b0 0x0001b0 RW  0x8
  NOTE           0x000254 0x0000000000400254 0x0000000000400254 0x000020 0x000020 R   0x4
  GNU_EH_FRAME   0x00067c 0x000000000040067c 0x000000000040067c 0x000034 0x000034 R   0x4
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x10
  GNU_RELRO      0x000e18 0x0000000000401e18 0x0000000000401e18 0x0001e8 0x0001e8 RW  0x8

 Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
   02     .interp .note.ABI-tag .dynsym .dynstr .gnu.hash .hash .gnu.version .gnu.version_r .rela.dyn .init .text .fini .rodata .eh_frame .eh_frame_hdr
   03     .fini_array .init_array .dynamic .got .got.plt .data .bss
   04     .dynamic
   05     .note.ABI-tag
   06     .eh_frame_hdr
   07
   08     .fini_array .init_array .dynamic .got .got.plt

这样做可以优化内核加载可执行文件时需要执行的mmap数量,但会牺牲一定的安全性:位于.rodata中的数据不应该被执行,但实际上却是(因为它与.text合并在了一起,而.text必须是可执行的)。这可能会显著增加尝试劫持进程的攻击面。

特别是在使用LLD链接二进制文件的新Linux系统中,更注重安全而非速度,将ELF头和.rodata放入第一个只读段中,从而产生3个加载段并提高了安全性。以下是典型的映射:

$ echo "int foo; int main() { return 0;}"  | clang -xc - -o a.out-lld -fuse-ld=lld
$ readelf -Wl a.out-lld

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

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  PHDR           0x000040 0x0000000000200040 0x0000000000200040 0x000230 0x000230 R   0x8
  INTERP         0x000270 0x0000000000200270 0x0000000000200270 0x00001c 0x00001c R   0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x000000 0x0000000000200000 0x0000000000200000 0x000558 0x000558 R   0x1000
  LOAD           0x001000 0x0000000000201000 0x0000000000201000 0x000185 0x000185 R E 0x1000
  LOAD           0x002000 0x0000000000202000 0x0000000000202000 0x001170 0x002005 RW  0x1000
  DYNAMIC        0x003010 0x0000000000203010 0x0000000000203010 0x000150 0x000150 RW  0x8
  GNU_RELRO      0x003000 0x0000000000203000 0x0000000000203000 0x000170 0x001000 R   0x1
  GNU_EH_FRAME   0x000440 0x0000000000200440 0x0000000000200440 0x000034 0x000034 R   0x1
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0
  NOTE           0x00028c 0x000000000020028c 0x000000000020028c 0x000020 0x000020 R   0x4

 Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
   02     .interp .note.ABI-tag .rodata .dynsym .gnu.version .gnu.version_r .gnu.hash .hash .dynstr .rela.dyn .eh_frame_hdr .eh_frame
   03     .text .init .fini
   04     .data .tm_clone_table .fini_array .init_array .dynamic .got .bss
   05     .dynamic
   06     .fini_array .init_array .dynamic .got
   07     .eh_frame_hdr
   08
   09     .note.ABI-tag

新版BFD-ld(我的版本是2.31.1)也不甘落后,将ELF头和.rodata设为只读,但未能将两个仅包含R的段合并为一个段,导致有4个可加载段:

$ echo "int foo; int main() { return 0;}"  | clang -xc - -o a.out-bfd -fuse-ld=bfd
$ readelf -Wl a.out-bfd

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   Flg Align
  PHDR           0x000040 0x0000000000400040 0x0000000000400040 0x000268 0x000268 R   0x8
  INTERP         0x0002a8 0x00000000004002a8 0x00000000004002a8 0x00001c 0x00001c R   0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x000000 0x0000000000400000 0x0000000000400000 0x0003f8 0x0003f8 R   0x1000
  LOAD           0x001000 0x0000000000401000 0x0000000000401000 0x00018d 0x00018d R E 0x1000
  LOAD           0x002000 0x0000000000402000 0x0000000000402000 0x000110 0x000110 R   0x1000
  LOAD           0x002e40 0x0000000000403e40 0x0000000000403e40 0x0001e8 0x0001f0 RW  0x1000
  DYNAMIC        0x002e50 0x0000000000403e50 0x0000000000403e50 0x0001a0 0x0001a0 RW  0x8
  NOTE           0x0002c4 0x00000000004002c4 0x00000000004002c4 0x000020 0x000020 R   0x4
  GNU_EH_FRAME   0x002004 0x0000000000402004 0x0000000000402004 0x000034 0x000034 R   0x4
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x10
  GNU_RELRO      0x002e40 0x0000000000403e40 0x0000000000403e40 0x0001c0 0x0001c0 R   0x1

 Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
   02     .interp .note.ABI-tag .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn
   03     .init .text .fini
   04     .rodata .eh_frame_hdr .eh_frame
   05     .init_array .fini_array .dynamic .got .got.plt .data .bss
   06     .dynamic
   07     .note.ABI-tag
   08     .eh_frame_hdr
   09
   10     .init_array .fini_array .dynamic .got

最后,一些选择受到--(no)rosegment(或对于BFD ld-Wl,z,noseparate-code)链接器选项的影响。


2
对于现代的binutils(bfd) (v2.31.1+),请使用-Wl,-z,noseparate-code来将可加载段的数量减少到2个。参考:https://github.com/bminor/binutils-gdb/commit/f6aec96dce1ddbd8961a3aa8a2925db2021719bb - Nehal J Wani

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接