代码之家  ›  专栏  ›  技术社区  ›  tuket

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

  •  0
  • tuket  · 技术社区  · 4 年前

    我制作了这个简单的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_filesz fileSize 。这意味着该段包含整个文件。这是我没想到的( and I'm not the only one ),但显然Linux操作系统需要标头处于以下类型的段中 PT_LOAD 从而加载信息。

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

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

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

    enter image description here

    但我不明白为什么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 $ - $$
    
    0 回复  |  直到 4 年前
        1
  •  7
  •   Employed Russian    4 年前

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

    .

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

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

    In the source ,你可以看到,事实上程序头 需要成为任何 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_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
    

    附言:您正在将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) .