首页 > 解决方案 > 在 ELF 中,为什么标题需要在一个段中?

问题描述

我制作了这个简单的 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_offset0 和。这意味着该段包含整个文件。这是我没有预料到的(而且我不是唯一的),但显然 Linux 操作系统需要标头位于类型段中,以便加载信息。p_fileszfileSizePT_LOAD

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

关于段要强调的重要一点是只有 PT_LOAD 段被加载到内存中。因此,每隔一个段映射到 PT_LOAD 段之一的内存范围内。

为了理解 Sections 和 Segments 之间的关系,我们可以将 Segments 想象成一种使 linux 加载器的生活更轻松的工具,因为它们通过属性将 section 分组为单个段,以使可执行文件的加载过程更高效,而不是将每个单独的部分加载到内存中。下图试图说明这个概念:

在此处输入图像描述

但我不明白为什么 Linux 需要在运行时加载这些标头。它们是用来做什么的?如果进程运行需要它们,Linux不能自己加载它吗?

编辑:

评论中已经提到不需要加载标题,但是,有时无论如何都会加载它们以避免必须添加填充。我尝试添加填充以使其对齐 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 $ - $$

标签: linuxassemblyelf

解决方案


但我不明白为什么 Linux 需要在运行时加载这些标头。

没有

它们是用来做什么的?如果进程运行需要它们,Linux不能自己加载它吗?

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

源代码中,您可以看到实际上程序头不需要是任何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

生成一个运行良好的程序,但程序头不在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

我试过添加填充

你没有正确地做到这一点。使用“带填充”源会导致以下结果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_LOADSIGSEGV

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

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

PS 您将 64 位程序链接到0x08048000,这是i*86(32 位)可执行文件的传统加载地址。x86_64二进制文件传统上从0x400000.

更新:

关于第一个示例,p_filesz 仍然是 fileSize,我认为应该超出文件的边界。

这是正确的:p_filesz并且p_memsz应该减少标题的大小(0x78此处)。请注意,这两个都将被四舍五入到页面大小(在添加 之后p_offset),因此对于此示例,没有实际区别。

更新 2:

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).


推荐阅读