在 ELF 中,为什么 headers 需要在一个段中?

In ELF, why do the headers need to be in one segment?

出于学习目的,我制作了这个简单的 ELF:

bits 64
org 0x08048000

elfHeader:
    db  0x7F, "ELF", 2, 1, 1, 0   ; e_ident
    db 0                            ; abi version
    times 7 db 0                    ; unused padding
    dw  2                         ; e_type
    dw  62                        ; e_machine
    dd  1                         ; e_version
    dq  _start                    ; e_entry
    dq  programHeader - $$        ; e_phoff
    dq  0                         ; e_shoff
    dd  0                         ; e_flags
    dw  elfHeaderSize             ; e_ehsize
    dw  programHeaderSize         ; e_phentsize
    dw  1                         ; e_phnum
    dw  0                         ; e_shentsize
    dw  0                         ; e_shnum
    dw  0                         ; e_shstrndx

elfHeaderSize  equ $ - elfHeader

programHeader:
    dd  1                         ; p_type
    dd  7                         ; p_flags
    dq  0                         ; p_offset
    dq  $$                        ; p_vaddr
    dq  $$                        ; p_paddr
    dq  fileSize                  ; p_filesz
    dq  fileSize                  ; p_memsz
    dq  0x1000                    ; p_align

programHeaderSize equ  $ - programHeader

_start:
   xor rdi, rdi
   xor eax,eax
   mov al,60
   syscall

fileSize      equ     $ - $$

为了编译我使用的代码 NASM:

nasm -f bin exe.asm -o exe

如果你看一下 programHeader,你会看到 p_offset 是 0,而 p_fileszfileSize。这意味着该段包含整个文件。这是我没有预料到的(and I'm not the only one),但显然 Linux 操作系统需要 headers 位于 PT_LOAD 类型的段中,以便加载信息。

这是我能找到的唯一提到 headers 在一个段内这一事实的资源:https://www.intezer.com/blog/research/executable-linkable-format-101-part1-sections-segments/

Something important to highlight about segments is that only PT_LOAD segments get loaded into memory. Therefore, every other segment is mapped within the memory range of one of the PT_LOAD segments.

In order to understand the relationship between Sections and Segments, we can picture segments as a tool to make the linux loader’s life easier, as they group sections by attributes into single segments in order to make the loading process of the executable more efficient, instead of loading each individual section into memory. The following diagram attempts to illustrate this concept:

但我不明白为什么 Linux 需要 headers 在 运行 时加载。它们的用途是什么?如果进程需要它们运行,难道不能Linux自己加载吗?

编辑:

评论中已经提到 headers 不需要加载,但是,有时无论如何都会加载它们以避免必须添加填充。我尝试添加填充以使其对齐 4KB,但没有用。这是我的尝试:

bits 64
org 0x08048000

elfHeader:
    db  0x7F, "ELF", 2, 1, 1, 0   ; e_ident
    db 0                            ; abi version
    times 7 db 0                    ; unused padding
    dw  2                         ; e_type
    dw  62                        ; e_machine
    dd  1                         ; e_version
    dq  _start                    ; e_entry
    dq  programHeader - $$        ; e_phoff
    dq  0                         ; e_shoff
    dd  0                         ; e_flags
    dw  elfHeaderSize             ; e_ehsize
    dw  programHeaderSize         ; e_phentsize
    dw  1                         ; e_phnum
    dw  0                         ; e_shentsize
    dw  0                         ; e_shnum
    dw  0                         ; e_shstrndx

elfHeaderSize  equ $ - elfHeader

programHeader:
    dd  1                         ; p_type
    dd  7                         ; p_flags
    dq  _start - $$               ; p_offset
    dq  $$                        ; p_vaddr
    dq  $$                        ; p_paddr
    dq  codeSize                  ; p_filesz
    dq  codeSize                  ; p_memsz
    dq  0x1000                    ; p_align

programHeaderSize equ  $ - programHeader

; padding until 4KB
paddingUntil4k equ 4*1024 - ($ - elfHeader)
times paddingUntil4k db 0


_start:
   xor rdi, rdi
   xor eax,eax
   mov al,60
   syscall

codeSize equ $ - _start
fileSize equ $ - $$

But I don't understand why Linux needs that headers to be loaded at run time.

不会

What are they used for? If they are needed for the process to run, couldn't Linux load it by himself?

要回答所有这些问题,您需要查看 Linux 内核源代码。

the source 中,您可以看到实际上程序 header 做 而不是 需要成为任何 PT_LOAD 段的一部分,并且内核将自行读取它们。

像这样更改您的原始程序:

diff -u exe.asm.orig exe.asm
--- exe.asm.orig        2021-02-07 18:54:34.449336515 -0800
+++ exe.asm     2021-02-07 18:53:19.773532451 -0800
@@ -24,9 +24,9 @@
 programHeader:
     dd  1                         ; p_type
     dd  7                         ; p_flags
-    dq  0                         ; p_offset
-    dq  $$                        ; p_vaddr
-    dq  $$                        ; p_paddr
+    dq  _start - $$               ; p_offset
+    dq  _start                    ; p_vaddr
+    dq  _start                    ; p_paddr
     dq  fileSize                  ; p_filesz
     dq  fileSize                  ; p_memsz
     dq  0x1000                    ; p_align

生成一个 运行 没问题的程序,但其中的程序 header 不在 PT_LOAD 段中:

 eu-readelf --all exe
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
  Ident Version:                     1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           AMD x86-64
  Version:                           1 (current)
  Entry point address:               0x8048078
  Start of program headers:          64 (bytes into file)
  Start of section headers:          0 (bytes into file)
  Flags:
  Size of this header:               64 (bytes)
  Size of program header entries:    56 (bytes)
  Number of program headers entries: 1
  Size of section header entries:    0 (bytes)
  Number of section headers entries: 0 ([0] not available)
  Section header string table index: 0

Section Headers:
[Nr] Name                 Type         Addr             Off      Size     ES Flags Lk Inf Al

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  LOAD           0x000078 0x0000000008048078 0x0000000008048078 0x000081 0x000081 RWE 0x1000

I have tried adding padding

你没有做对。使用您的“带填充”源会产生以下 exe-padding:

...
  Entry point address:               0x8049000
...
Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  LOAD           0x001000 0x0000000008048000 0x0000000008048000 0x000009 0x000009 RWE 0x1000

这个二进制文件由内核启动,并立即跳转到起始地址0x8049000没有映射(因为它没有被PT_LOAD段),导致立即SIGSEGV.

要解决这个问题,您需要调整入口地址:

diff -u exe-padding.asm.orig exe-padding.asm
--- exe-padding.asm.orig        2021-02-07 18:57:31.800871195 -0800
+++ exe-padding.asm     2021-02-07 19:34:27.303071700 -0800
@@ -8,7 +8,7 @@
     dw  2                         ; e_type
     dw  62                        ; e_machine
     dd  1                         ; e_version
-    dq  _start                    ; e_entry
+    dq  _start - 0x1000           ; e_entry
     dq  programHeader - $$        ; e_phoff
     dq  0                         ; e_shoff
     dd  0                         ; e_flags

这再次生成了一个可运行的可执行文件。备案:

eu-readelf --all exe-padding
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
  Ident Version:                     1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           AMD x86-64
  Version:                           1 (current)
  Entry point address:               0x8048000
  Start of program headers:          64 (bytes into file)
  Start of section headers:          0 (bytes into file)
  Flags:                             
  Size of this header:               64 (bytes)
  Size of program header entries:    56 (bytes)
  Number of program headers entries: 1
  Size of section header entries:    0 (bytes)
  Number of section headers entries: 0 ([0] not available)
  Section header string table index: 0

Section Headers:
[Nr] Name                 Type         Addr             Off      Size     ES Flags Lk Inf Al

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  LOAD           0x001000 0x0000000008048000 0x0000000008048000 0x000009 0x000009 RWE 0x1000

P.S。您正在 0x08048000 链接您的 64 位程序,这是 i*86(32 位)可执行文件的传统加载地址。 x86_64 二进制文件通常从 0x400000.

开始

更新:

About the first example, p_filesz is still fileSize, I think that should get outside of the boundaries of the file.

这是正确的:p_fileszp_memsz 应该减少 header 的大小(这里是 0x78)。请注意,这两者都将四舍五入为页面大小(在添加 p_offset 之后),因此对于此示例,没有实际差异。

更新二:

pastebin.ubuntu.com/p/rgfVMrbcmJ

这导致以下 LOAD 段:

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  LOAD           0x000078 0x0000000008048000 0x0000000008048000 0x000081 0x000081 RWE 0x1000

这个二进制文件不会 运行(内核会拒绝它),因为它要求内核做不可能的事情:将 mmap 字节偏移 0x78 到页面开始。

如果应用程序执行等效的 mmap 调用,它会得到 EINVAL 错误,因为 mmap 要求 (offset % pagesize) == (addr % pagesize).