一直想看看这个,正好现在学习大跃进,索性一起看了吧。
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:段的类型,如
LOAD
、DYNAMIC
、INTERP
等。 - 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
:退出程序。
- .global _start:定义了程序的入口点
.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