背景
Linux应用层存在大量的应用启动过程,这个过程一般是通过以下两个函数实现的。
- int execve(const char *filename, char *const argv[], char *const envp[]);
- int execveat(int dirfd, const char *pathname, char *const argv[], char *const envp[], int flags);
而这两个函数的入口参数filename和pathname,其实指向的都是ELF格式的文件(.o,.so,和可执行文件)。也就是说,本质上来说,程序的加载运行就是对ELF格式文件的解析,找到程序内部资源信息和函数入口,并依据此信息初始化内存后,在内存相对封闭的环境内进行程序执行,此时若程序跑飞,仅仅需要对此程序占用的内存做销毁处理即可(不考虑函数内部存在动态内存申请释放的情况下),并不会影响整个系统的稳定性。
ELF文件解析
和wav等文件类型一样,elf文件也由文件头(表明文件类型和文件的核心参数)和内容部分(程序首部(Program header table),节(Section)和节首部(Section header table))组成。其在数据存储分布上存在以下规律:
可以通过readelf -a 来查看elf文件中的所有信息。
ELF头解析
ELF头长度固定,总共占用32字节(32位系统)或64字节(64位系统),其各段分布如下:
名称 |
长度(字节) |
功能描述 |
e_ident |
4 |
第一个字节为0x7F,表示可删除的ASCII编码 第二至四字节为ELF(0x45,0x4C,0x46),代表此文件为ELF文件 |
1 |
1表示32为的ELF,2代表64位的ELF |
|
1 |
字节序(大端还是小端对齐等) |
|
1 |
版本 |
|
1 |
表示应用二进制接口(ABI)的类型 |
|
8 |
暂未使用,填充0 |
|
e_type |
2 |
ELF文件类型,值的含义如下: 0表示没有文件类型 1表示可重定位文件(目标文件) 2表示可执行文件 3表示动态库(.so) 4表示核心转存储文件 0xFF00表示用于特定处理器的语义 0xFFFF表示用于特定处理器的语义 |
e_machine |
2 |
机器类别,区分执行平台,X86,ARM,ARM64等 |
e_version |
4 |
版本,用于区分不同的ELF变体,目前的规范文件只定义了版本1 |
e_entry |
4/8 |
程序入口的虚拟地址,0代表这个elf没有关联的入口 |
e_phoff |
4/8 |
程序(Program)首部表的文件偏移 |
e_shoff |
4/8 |
节(Section)首部表的文件偏移 |
e_flags |
4 |
处理器特定的标记 |
e_ehsize |
2 |
ELF首部的长度,值为arm32为 0x34(52字节),arm64为0x40(64字节) |
e_phentsize |
2 |
程序首部表中表项(Segment)的长度,单位是字节 |
e_phnum |
2 |
程序首部表中表项的数量 |
e_shensize |
2 |
节首部表中表项的长度,单位是字节 |
e_shnum |
2 |
节首部表中表项的数量 |
e_shstrndx |
2 |
节名称字符串表(.shstrtab)在节首部表中的索引,即代表elf文件中的一个section(字符串表),里面存放了section name |
可以通过readelf -h 来查看elf文件中的ELF头信息
程序首部表解析
程序首部表中存在多个表项,每个表项的长度都为32字节(32位系统)或48字节(64位系统)
名称 |
长度(字节) |
功能描述 |
p_type |
4 |
段的类型,常见的如下: 1表示可加载段(PT_LOAD),表示可被加载到内存的段,如代码和数据 3表示解释器段(PT_INTERP),指定把可执行文件映射到虚拟地址空间以后必须调用的解释器,解释器负责链接动态库和解析没有解析的符号。解释器通常是动态链接器,即ld共享库,负责把程序以来的动态库映射到虚拟地址空间 |
p_flags |
4 |
段的标志,常用的3个权限标志是读、写和执行 |
p_offset |
4/8 |
段在ELF文件中的偏移 |
p_vaddrp_vaddr |
4/8 |
段映射到内存后的虚拟地址 |
p_paddr |
4/8 |
段映射到内存后的物理地址 |
p_filez |
4 |
段在ELF文件中占用的长度 |
p_memsz |
4 |
段在内存中占用的长度 |
p_align |
4 |
段的对齐值,p_vaddr和p_paddr对p_align取模后为0 |
可以通过readelf -l 来查看elf文件中的程序首部表信息
节首部表解析
同样的,节首部表也是固定长度的,其占用40字节(32位系统)或48字节(64位系统)。
名称 |
长度(字节) |
功能含义 |
sh_name |
4 |
所指向的节的名字 |
sh_type |
4 |
所指向的节的类型 |
sh_flags |
4 |
所指向的节的属性 |
sh_addr |
4/8 |
所指向节在执行时的虚拟地址 |
sh_offset |
4/8 |
所指向的字节在ELF文件中的偏移量 |
sh_size |
4 |
所指向的字节占用的字节数 |
sh_link |
4 |
关联节头的下表索引 |
sh_info |
4 |
附加的节信息 |
sh_addralign |
4 |
节对齐值 |
sh_entsize |
4 |
如果节包含一个表项长度固定的表,如符号表,那么这个成员存放表项的长度 |
可以通过readelf -S 来查看elf文件中的节首部表信息
重要的节及说明
名称 |
说明 |
.text |
代码节(也称文本节),通常称代码段,包含程序的机器指令 |
.data |
数据节,也称数据段,包含已初始化的数据,程序在运行器件可以修改 |
.rodata |
只读数据 |
.bss |
没有初始化的数据,在程序开始运行前用0填充 |
.interp |
保存解释器的名称,通常是动态链接库(ld共享库) |
.shstrtab |
节名称字符串表 |
.symtab |
符号表。符号包括函数和全局变量,符号名称存放在字符串表中,符号表存储符号名称在字符串表里的偏移。可以通过readelf --symbols 查看 |
.strtab |
字符串表,存放符号表所需的字符串 |
.init |
程序初始化时执行的机器指令 |
.fini |
程序结束时执行的机器指令 |
.dynamic |
存放动态链接信息,包含程序依赖的所有动态库,这是动态链接器需要的信息,可以通过readelf --dynamic 查看 |
.dynsym |
存放动态符号表,包含需要动态链接的所有符号,即程序所引用的动态库里面的函数和全局变量,这是动态链接器需要的信息,可以通过readelf --dyn-syms 查看 |
.dynstr |
存放一个字符串表,包含动态链接需要的所有字符串,即动态库的名称、函数名称和全局变量的名称。.dynamic节不直接存储动态库的名称,而是存储库名称在该字符串表里的偏移 |
.rel.xxxx或.rela.xxxx |
用于xxxx节区的重定位信息,记录了需要在链接时修改的指令 |
部分节内容格式
.symtab
如果节是符号表类型,其内部存储的数据便遵循以下规则:
名称 |
长度(字节) |
功能含义 |
st_name |
4 |
符号的名字 |
st_value |
4 |
符号相对于其所在Section偏移的相对地址 |
st_size |
4 |
符号所占用的字节数 |
st_info |
1 |
低四位表示符号的作用范围(bit0:全局或局部,bit1:是否是弱引用),高四位表示符号的类型(变量、函数等) |
st_other |
1 |
没有意义 |
st_shndx |
2 |
该符号的值在哪个Section下存储 |
知识点确认
简单的测试程序编写
- #include <stdio.h>
-
- int main(char argc, char *argv)
- {
- int a = 0;
- return a;
- }
编写makefile文件
- TARGET = test
- CC = gcc
- CFLAGS = -Wall -g
- SRC = test.c
- OBJ = $(SRC:.c=.o)
-
- all: $(TARGET)
-
- $(TARGET): $(OBJ)
- $(CC) $(CFLAGS) -o $@ $^
-
- %.o: %.c
- $(CC) $(CFLAGS) -c $< -o $@
-
- clean:
- rm -f $(OBJ) $(TARGET)
-
- .PHONY: all clean
通过运行make后生成test文件,
由于readelf -a命令输出的内容过多,虽然能够较为全面的看清楚elf文件内容,但不便于文档展开分析,因此直接查看具体细节的命令。
查看ELF头
readelf -h test
- 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: 0x400400
- Start of program headers: 64 (bytes into file)
- Start of section headers: 5128 (bytes into file)
- Flags: 0x0
- Size of this header: 64 (bytes)
- Size of program headers: 56 (bytes)
- Number of program headers: 9
- Size of section headers: 64 (bytes)
- Number of section headers: 35
- Section header string table index: 32
其对应结构图下
经过对比,会发现其实readelf的作用就是帮助我们把枯燥的数据解码成直观易懂的提示内容。
查看程序首部表
readelf -l test
- Elf file type is EXEC (Executable file)
- Entry point 0x400400
- There are 9 program headers, starting at offset 64
-
- Program Headers:
- Type Offset VirtAddr PhysAddr
- FileSiz MemSiz Flags Align
- PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
- 0x00000000000001f8 0x00000000000001f8 R E 8
- INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
- 0x000000000000001c 0x000000000000001c R 1
- [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
- LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
- 0x00000000000006bc 0x00000000000006bc R E 200000
- LOAD 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
- 0x0000000000000228 0x0000000000000230 RW 200000
- DYNAMIC 0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
- 0x00000000000001d0 0x00000000000001d0 RW 8
- NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254
- 0x0000000000000044 0x0000000000000044 R 4
- GNU_EH_FRAME 0x0000000000000594 0x0000000000400594 0x0000000000400594
- 0x0000000000000034 0x0000000000000034 R 4
- GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
- 0x0000000000000000 0x0000000000000000 RW 10
- GNU_RELRO 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
- 0x00000000000001f0 0x00000000000001f0 R 1
-
- Section to Segment mapping:
- Segment Sections...
- 00
- 01 .interp
- 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
- 03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
- 04 .dynamic
- 05 .note.ABI-tag .note.gnu.build-id
- 06 .eh_frame_hdr
- 07
- 08 .init_array .fini_array .jcr .dynamic .got
以其中第一个程序首部为例,其对应数据结构如下(具体内部每段含义,可以对照程序首部表格式对照查看):
查看节首部表
- readelf -S test
- There are 35 section headers, starting at offset 0x1408:
-
- Section Headers:
- [Nr] Name Type Address Offset
- Size EntSize Flags Link Info Align
- [ 0] NULL 0000000000000000 00000000
- 0000000000000000 0000000000000000 0 0 0
- [ 1] .interp PROGBITS 0000000000400238 00000238
- 000000000000001c 0000000000000000 A 0 0 1
- [ 2] .note.ABI-tag NOTE 0000000000400254 00000254
- 0000000000000020 0000000000000000 A 0 0 4
- [ 3] .note.gnu.build-i NOTE 0000000000400274 00000274
- 0000000000000024 0000000000000000 A 0 0 4
- [ 4] .gnu.hash GNU_HASH 0000000000400298 00000298
- 000000000000001c 0000000000000000 A 5 0 8
- [ 5] .dynsym DYNSYM 00000000004002b8 000002b8
- 0000000000000048 0000000000000018 A 6 1 8
- [ 6] .dynstr STRTAB 0000000000400300 00000300
- 0000000000000038 0000000000000000 A 0 0 1
- [ 7] .gnu.version VERSYM 0000000000400338 00000338
- 0000000000000006 0000000000000002 A 5 0 2
- [ 8] .gnu.version_r VERNEED 0000000000400340 00000340
- 0000000000000020 0000000000000000 A 6 1 8
- [ 9] .rela.dyn RELA 0000000000400360 00000360
- 0000000000000018 0000000000000018 A 5 0 8
- [10] .rela.plt RELA 0000000000400378 00000378
- 0000000000000030 0000000000000018 A 5 12 8
- [11] .init PROGBITS 00000000004003a8 000003a8
- 000000000000001a 0000000000000000 AX 0 0 4
- [12] .plt PROGBITS 00000000004003d0 000003d0
- 0000000000000030 0000000000000010 AX 0 0 16
- [13] .text PROGBITS 0000000000400400 00000400
- 0000000000000182 0000000000000000 AX 0 0 16
- [14] .fini PROGBITS 0000000000400584 00000584
- 0000000000000009 0000000000000000 AX 0 0 4
- [15] .rodata PROGBITS 0000000000400590 00000590
- 0000000000000004 0000000000000004 AM 0 0 4
- [16] .eh_frame_hdr PROGBITS 0000000000400594 00000594
- 0000000000000034 0000000000000000 A 0 0 4
- [17] .eh_frame PROGBITS 00000000004005c8 000005c8
- 00000000000000f4 0000000000000000 A 0 0 8
- [18] .init_array INIT_ARRAY 0000000000600e10 00000e10
- 0000000000000008 0000000000000000 WA 0 0 8
- [19] .fini_array FINI_ARRAY 0000000000600e18 00000e18
- 0000000000000008 0000000000000000 WA 0 0 8
- [20] .jcr PROGBITS 0000000000600e20 00000e20
- 0000000000000008 0000000000000000 WA 0 0 8
- [21] .dynamic DYNAMIC 0000000000600e28 00000e28
- 00000000000001d0 0000000000000010 WA 6 0 8
- [22] .got PROGBITS 0000000000600ff8 00000ff8
- 0000000000000008 0000000000000008 WA 0 0 8
- [23] .got.plt PROGBITS 0000000000601000 00001000
- 0000000000000028 0000000000000008 WA 0 0 8
- [24] .data PROGBITS 0000000000601028 00001028
- 0000000000000010 0000000000000000 WA 0 0 8
- [25] .bss NOBITS 0000000000601038 00001038
- 0000000000000008 0000000000000000 WA 0 0 1
- [26] .comment PROGBITS 0000000000000000 00001038
- 000000000000002b 0000000000000001 MS 0 0 1
- [27] .debug_aranges PROGBITS 0000000000000000 00001063
- 0000000000000030 0000000000000000 0 0 1
- [28] .debug_info PROGBITS 0000000000000000 00001093
- 00000000000000c0 0000000000000000 0 0 1
- [29] .debug_abbrev PROGBITS 0000000000000000 00001153
- 000000000000006b 0000000000000000 0 0 1
- [30] .debug_line PROGBITS 0000000000000000 000011be
- 000000000000003b 0000000000000000 0 0 1
- [31] .debug_str PROGBITS 0000000000000000 000011f9
- 00000000000000c7 0000000000000001 MS 0 0 1
- [32] .shstrtab STRTAB 0000000000000000 000012c0
- 0000000000000148 0000000000000000 0 0 1
- [33] .symtab SYMTAB 0000000000000000 00001cc8
- 0000000000000678 0000000000000018 34 50 8
- [34] .strtab STRTAB 0000000000000000 00002340
- 0000000000000224 0000000000000000 0 0 1
- Key to Flags:
- W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
- I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
- O (extra OS processing required) o (OS specific), p (processor specific)
以其中第二个节为例(具体内部每段含义,可以对照字节首部表格式对照查看):
总结
至此,elf文件的文件格式基本上可以摸清。由于数据各种地址指来指去,数据长度也各种可变,很不便于直接分析固件,好在有对应的额readelf工具,可以程式化的辅助我们解析elf文件,而不必关心文件内某段内部的具体跳转细节。