ELF文件格式

一直想看看这个,正好现在学习大跃进,索性一起看了吧。

ELF文件就是可执行文件了,Executable and Linkable Format。linux下可执行程序,so都是这个格式。由4部分组成,分别是ELF头(ELF header)、程序头表(Program header table)、节(Section)和节头表(Section header table)。ELF头肯定有,程序头表也肯定有。

ELF文件由多个段(segments)和节(sections)组成。段是运行时使用的最小单元,而节是编译时使用的最小单元。一个段可以包含一个或多个节。

段描述了程序在内存中的布局,而节则包含了程序的静态数据、符号表等。这些段和节的组织通过头部信息来描述,包括程序头部表和节头部表,它们存储了关于文件中各个段和节的位置、大小和其他属性信息。‌

1 ELF 文件头(ELF Header)

ELF文件头包含了识别和解释文件内容的信息。它是整个文件的开始部分,定义了文件的类型、机器架构、入口点地址以及程序头表和节头表的偏移等。

主要字段包括:

  • e_ident:包含魔数、文件类(32位或64位)、数据编码(小端或大端)等信息。
  • e_type:文件类型(可重定位文件、可执行文件、共享对象文件、核心转储文件)。
  • e_machine:目标机器架构(如x86、x86-64、ARM等)。
  • e_version:文件版本。
  • e_entry:程序的入口点地址。
  • e_phoff:程序头表的偏移量。
  • e_shoff:节头表的偏移量。
  • e_flags:与处理器相关的标志。
  • e_ehsize:ELF文件头的大小。
  • e_phentsize:程序头表中每个条目的大小。
  • e_phnum:程序头表中的条目数。
  • e_shentsize:节头表中每个条目的大小。
  • e_shnum:节头表中的条目数。
  • e_shstrndx:节头字符串表的索引。

2 程序头表(Program Header Table)

程序头表描述了段的加载信息。每个条目都描述了一个段,包括其在文件中的位置、内存地址、文件大小、内存大小和段的标志(如可读、可写、可执行)。

主要字段包括:

  • p_type:段的类型(如加载段、动态段、解释器段等)。
  • p_offset:段在文件中的偏移量。
  • p_vaddr:段在内存中的虚拟地址。
  • p_paddr:段在内存中的物理地址(一般未使用)。
  • p_filesz:段在文件中的大小。
  • p_memsz:段在内存中的大小。
  • p_flags:段的标志(可读、可写、可执行)。
  • p_align:段在内存中的对齐要求。

3 节头表(Section Header Table)

节头表描述了文件的各个节。每个条目都描述了一个节,包括其名称、类型、地址、偏移量、大小和标志。

主要字段包括:

  • sh_name:节名称的索引(在节名称字符串表中)。
  • sh_type:节的类型(如符号表、字符串表、重定位表等)。
  • sh_flags:节的标志(可写、可分配、可执行等)。
  • sh_addr:节在内存中的地址。
  • sh_offset:节在文件中的偏移量。
  • sh_size:节的大小。
  • sh_link:与此节相关的另一个节的索引。
  • sh_info:附加信息。
  • sh_addralign:节的对齐要求。
  • sh_entsize:如果节包含固定大小的条目,此值为条目大小。

常见的节(Sections)

  • .text:包含可执行的代码。
  • .data:包含已初始化的全局和静态变量。
  • .bss:包含未初始化的全局和静态变量。
  • .rodata:只读数据。
  • .symtab:符号表。
  • .strtab:字符串表。
  • .rel.text.rela.text:重定位信息。
  • .debug:调试信息。

使用readelf这个工具,基本上可以看到相关信息。

看elf文件头:

ubuntu@VM-8-10-ubuntu:~/test/cpp$ readelf -h a.out
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - GNU
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x11a0
  Start of program headers:          64 (bytes into file)
  Start of section headers:          37880 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         13
  Size of section headers:           64 (bytes)
  Number of section headers:         33
  Section header string table index: 32

看程序头表:

ubuntu@VM-8-10-ubuntu:~/test/cpp$ readelf -l a.out

Elf file type is DYN (Shared object file)
Entry point 0x11a0
There are 13 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x00000000000002d8 0x00000000000002d8  R      0x8
  INTERP         0x0000000000000318 0x0000000000000318 0x0000000000000318
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000d40 0x0000000000000d40  R      0x1000
  LOAD           0x0000000000001000 0x0000000000001000 0x0000000000001000
                 0x0000000000001935 0x0000000000001935  R E    0x1000
  LOAD           0x0000000000003000 0x0000000000003000 0x0000000000003000
                 0x0000000000001315 0x0000000000001315  R      0x1000
  LOAD           0x0000000000004c68 0x0000000000005c68 0x0000000000005c68
                 0x00000000000003b0 0x00000000000004f0  RW     0x1000
  DYNAMIC        0x0000000000004d48 0x0000000000005d48 0x0000000000005d48
                 0x0000000000000210 0x0000000000000210  RW     0x8
  NOTE           0x0000000000000338 0x0000000000000338 0x0000000000000338
                 0x0000000000000020 0x0000000000000020  R      0x8
  NOTE           0x0000000000000358 0x0000000000000358 0x0000000000000358
                 0x0000000000000044 0x0000000000000044  R      0x4
  GNU_PROPERTY   0x0000000000000338 0x0000000000000338 0x0000000000000338
                 0x0000000000000020 0x0000000000000020  R      0x8
  GNU_EH_FRAME   0x00000000000031b0 0x00000000000031b0 0x00000000000031b0
                 0x0000000000000354 0x0000000000000354  R      0x4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x0000000000004c68 0x0000000000005c68 0x0000000000005c68
                 0x0000000000000398 0x0000000000000398  R      0x1

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

输出解释

  • Type:段的类型,如LOADDYNAMICINTERP等。
  • Offset:段在文件中的偏移量。
  • VirtAddr:段在内存中的虚拟地址。
  • PhysAddr:段在内存中的物理地址(通常未使用)。
  • FileSiz:段在文件中的大小。
  • MemSiz:段在内存中的大小。
  • Flags:段的标志,如R(可读)、W(可写)、E(可执行)。
  • Align:段在内存中的对齐要求。

给一个elf的汇编例子

  • .section .data:数据段,定义了程序的静态数据。
    • hello:一个字符串"Hello, World!\n"。
  • .section .text:代码段,包含可执行代码。
    • .global _start:定义了程序的入口点_start
    • _start:程序的入口点,包含两个系统调用:
      • write:将"Hello, World!"字符串写入标准输出。
      • exit:退出程序。
.section .data
hello:
    .asciz "Hello, World!\n"

.section .text
.global _start

_start:
    # Write "Hello, World!" to stdout
    mov $1, %rax             # syscall: write
    mov $1, %rdi             # file descriptor: stdout
    lea hello(%rip), %rsi    # buffer: hello
    mov $14, %rdx            # buffer length: 14
    syscall

    # Exit the program
    mov $60, %rax            # syscall: exit
    xor %rdi, %rdi           # exit code: 0
    syscall
  • mov $1, %rax:将系统调用号1(sys_write)放入寄存器rax
  • mov $1, %rdi:将文件描述符1(标准输出)放入寄存器rdi
  • lea hello(%rip), %rsi:将字符串的地址加载到寄存器rsi%rip是当前指令指针的寄存器,hello(%rip)是相对rip的偏移地址。
  • mov $14, %rdx:将字符串的长度14放入寄存器rdx
  • syscall:执行系统调用,参数为rax(系统调用号),rdi(文件描述符),rsi(缓冲区),和rdx(缓冲区长度)。
  • mov $60, %rax:将系统调用号60(sys_exit)放入寄存器rax
  • xor %rdi, %rdi:将寄存器rdi置为0,表示程序的退出状态码。
  • syscall:执行退出系统调用。

运行:

as -o hello.o hello.asm
ld -o hello hello.o
./hello

相关信息:

ubuntu@VM-8-10-ubuntu:~/test/cpp$ readelf -l hello

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

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000000e8 0x00000000000000e8  R      0x1000
  LOAD           0x0000000000001000 0x0000000000401000 0x0000000000401000
                 0x000000000000002a 0x000000000000002a  R E    0x1000
  LOAD           0x0000000000002000 0x0000000000402000 0x0000000000402000
                 0x000000000000000f 0x000000000000000f  RW     0x1000

 Section to Segment mapping:
  Segment Sections...
   00
   01     .text
   02     .data
ubuntu@VM-8-10-ubuntu:~/test/cpp$ readelf -h hello
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
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x401000
  Start of program headers:          64 (bytes into file)
  Start of section headers:          8504 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         3
  Size of section headers:           64 (bytes)
  Number of section headers:         6
  Section header string table index: 5
ubuntu@VM-8-10-ubuntu:~/test/cpp$ readelf -s hello

Symbol table '.symtab' contains 9 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000401000     0 SECTION LOCAL  DEFAULT    1
     2: 0000000000402000     0 SECTION LOCAL  DEFAULT    2
     3: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.o
     4: 0000000000402000     0 NOTYPE  LOCAL  DEFAULT    2 hello
     5: 0000000000401000     0 NOTYPE  GLOBAL DEFAULT    1 _start
     6: 000000000040200f     0 NOTYPE  GLOBAL DEFAULT    2 __bss_start
     7: 000000000040200f     0 NOTYPE  GLOBAL DEFAULT    2 _edata
     8: 0000000000402010     0 NOTYPE  GLOBAL DEFAULT    2 _end
ubuntu@VM-8-10-ubuntu:~/test/cpp$ readelf -S hello
There are 6 section headers, starting at offset 0x2138:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000401000  00001000
       000000000000002a  0000000000000000  AX       0     0     1
  [ 2] .data             PROGBITS         0000000000402000  00002000
       000000000000000f  0000000000000000  WA       0     0     1
  [ 3] .symtab           SYMTAB           0000000000000000  00002010
       00000000000000d8  0000000000000018           4     5     8
  [ 4] .strtab           STRTAB           0000000000000000  000020e8
       0000000000000027  0000000000000000           0     0     1
  [ 5] .shstrtab         STRTAB           0000000000000000  0000210f
       0000000000000027  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

ELF文件的运行过程

1. 程序启动

当用户在命令行输入一个可执行文件的名称时,Shell会调用系统调用execve()来启动该程序。execve()系统调用会将当前进程替换为新的可执行文件。

2. 内核加载器

当内核接收到execve()系统调用时,会执行以下步骤:

2.1 读取ELF头 内核读取ELF文件头(Elf Header),检查文件的魔数和类型(可执行文件、共享对象文件等)。

2.2 读取程序头表 根据ELF文件头中的信息,内核读取程序头表(Program Header Table),这表明了哪些部分需要加载到内存中以及它们的属性(可读、可写、可执行)。

2.3 加载段 内核根据程序头表中的信息,将可执行文件的段(Segments)加载到内存中。通常包括以下几种段: 可执行段(.text):包含可执行代码。 数据段(.data):包含已初始化的全局和静态变量。 BSS段(.bss):包含未初始化的全局和静态变量,内核会将这部分内存清零。 只读数据段(.rodata):包含只读数据。

2.4 设置内存映射 内核为各个段设置内存映射,包括虚拟地址到物理地址的映射,并设置适当的内存保护(如可读、可写、可执行)。

2.5 动态链接器(ld-linux) 如果ELF文件是动态链接的可执行文件,内核会查找并加载动态链接器(通常为ld-linux.so或类似名称),并将控制权交给动态链接器。

3. 动态链接器 动态链接器负责解析和链接动态库,并准备程序执行环境。

3.1 加载共享库 动态链接器读取可执行文件的动态段(Dynamic Section),找到所需的共享库(动态库),并将它们加载到内存中。

3.2 重定位 动态链接器处理可执行文件和共享库中的重定位条目,将符号地址更新为实际内存地址。这包括函数调用和变量引用的地址修正。

3.3 解析符号 动态链接器解析所有需要的符号,这些符号可能定义在共享库或可执行文件本身。

3.4 调用构造函数 动态链接器调用所有共享库中的全局构造函数(Global Constructors),通常是使用__attribute__((constructor))标记的函数。

4. 进入用户代码 当所有必要的库加载和重定位完成后,动态链接器将控制权交还给可执行文件的入口点(Entry Point),这是ELF头中e_entry字段指定的地址。

4.1 入口函数 可执行文件的入口函数通常是运行时启动代码(如C运行时库的_start函数)。这个函数会执行以下操作: 初始化堆栈。 初始化全局和静态变量。 调用main函数,传递命令行参数和环境变量。

4.2 main函数 main函数是用户代码的实际入口点,当main函数返回时,运行时启动代码会执行清理操作,并调用exit系统调用终止程序。

对了,有兴趣可以看看这个,手搓一个elf文件。

https://www.youtube.com/watch?v=JM9jX2aqkog

参考:

ChatGPT

ELF(文件格式)_百度百科

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值