背景
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文件,而不必关心文件内某段内部的具体跳转细节。