ELF文件格式说明,以及objdump和readelf指令的常用选项。
ELF文件格式概述
ELF文件格式是链接、装载的基础。ELF格式是在UNIX System V中引入的,用来支持C++和动态链接,他的前身是a.out格式。现在ELF被广泛用于Linux、BSD等类UNIX系统。下面是一些常见的目标文件格式,有关这些格式的详细说明,可以参考相关文档或《链接器和加载器》第3章。
空目标文件格式:MS-DOS的COM文件
代码区段:UNIX的a.out文件
重定位:MS-DOS的EXE文件
可重定位的a.out格式
UNIX的ELF格式
IBM 360目标格式
微软PE格式
Intel/Microsoft的OMF文件格式
ELF学习资料
示例代码
为了更好的理解ELF文件格式,会使用以下代码编译出来的文件作演示。编译完成之后,会得到两个文件tiny_hello.o和tiny_hello。代码和Makefile可以从网页tiny_hello 获取。注意,这份代码需要在32位Linux系统上运行。
编译指令:
1 2 gcc -c -g -fno-builtin tiny_hello.c ld -static -e nomain tiny_hello.o -o tiny_hello
源代码和Makefile如下:
ELF 文件类型
ELF将文件分为4类,下表对比了不同类型之间的差异。
类型
链接
加载
示例文件
说明
可重定位文件
Y
N
tiny_hello.o
由编译器和汇编器创建,运行前需要被链接器处理。
可执行文件
N
Y
tiny_hello
完成了所有的重定位工作和符号解析(除共享库符号)。
共享目标文件
Y
Y
libc.so.6
即包括链接器需要的符号信息,也包括运行时可以直接执行的代码。
核心转储文件
-
-
-
-
编译器、汇编器和链接器将ELF文件看作是Section header table描述的一系列逻辑区段(section)的集合;加载器将ELF文件看作是Program header table描述的一系列段(segment)的集合。一个段通常由多个区段组合。可重定位文件有Section header table,可执行文件有Program header table,共享目标文件两者都有。
ELF 文件头
ELF文件都以ELF文件头开始,在32位机器上通过结构体Elf32_Ehdr来描述。重点关注e_shoff和e_phoff,通过这两个字段可以寻找到区段头部表和程序头部表,从而可以定位到其余所有的区段和段。
通过通过指令readelf -h elffile来查看ELF文件头。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: REL (Relocatable file) Machine: Intel 80386 Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 1464 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 40 (bytes) Number of section headers: 25 Section header string table index: 22
可重定位文件
可重定位文件可以看作是一系列在区段头部表(Section header table)中被定义的区段的集合。区段头部表是一个以Elf32_Shdr结构体为元素的数组,数组中的每一个元素对应一个段。
字段
说明
sh_name
区段名,存储在区段.shstrtab的偏移。
sh_type
区段类型
sh_flags
区段标志位
sh_addr
可加载区段在进程地址空间中的虚拟地址,不可加载区段为0。
sh_offset
区段在文件中的偏移。区段不在文件中则为0。
sh_size
区段的长度。
sh_link
段链接信息,存储的是相关信息的区段号。
sh_info
区段更多的信息。
sh_addralign
段地址对齐,2的指数倍。
sh_entsize
区段为一个表时,表项的大小。
区段类型
区段常见的类型有:
类型
说明
SHT_PROGBITS
程序内容,包括代码、数据和调试器信息。
SHT_NOBITS
在文件中没有分配空间,在程序加载时分配空间,例如.bss区段。
SHT_SYMTAB / SHT_DNYSYM
符号表 。SHT_SYMBAT包含所有的符号,SHT_DNYSYM包含动态链接的符号,会被加载到内存。 readelf -s objdump -t
SHT_STRTAB
字符串表。
SHT_REL / SHT_RELA
重定位表,包含重定位信息。 objdump -r objdump -R
SHT_DYNAMIC
动态链接信息。 readelf -d
SHT_HASH
哈希表。
区段标志位
区段标志位表示该区段在进程虚拟地址空间中的属性。
标志
说明
SHF_WRITE
在进程空间中可写。
SHF_ALLOC
在进程空间中要分配空间。
SHF_EXECINSTR
在进程空间中可以被执行。
根据区段类型和区段标志位的组合,ELF文件可能有如下区段:
.text,正文段,具有ALLOC+EXECINSTR属性的PROGBITS类型区段。
.data,数据段,具有ALLOC+WRITE属性的PROGBITS类型区段。
.rodata,只读数据段,具有ALLOC属性的PROGBITS类型区段。
.bss,具有ALLOC+WRITE属性的NOBITS类型区段。
.rel.text, .rel.data, .rel.rodata,REL或RELA类型区段,包含对应区段的重定位信息。
.init和.fini,具有ALLOC+EXECINSTR属性的PROGBITS类型区段,对C++来说时必须的。
.symtab,符号表,SYMTAB类型的区段。
.dnysym,动态链接符号表,具有ALLOC属性的DNYSYM类型区段。
.strtab,字符串表,STRTAB类型的区段,通常保存符号的字符串。
.shstrtab,段表字符串表,通常存储段名字符串。
.dnystr,ALLOC属性的STRTAB类型区段,通常保存动态链接符号的字符串。
.got
.plt
.line
.comment
.interp
区段头部表示例
可以使用指令readelf -S elffile 或objdump -h elffile,查看区段头部表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 There are 25 section headers, starting at offset 0x5b8: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .text PROGBITS 00000000 000034 000049 00 AX 0 0 4 [ 2] .rel.text REL 00000000 000bd4 000018 08 23 1 4 [ 3] .data PROGBITS 00000000 000080 000010 00 WA 0 0 4 [ 4] .rel.data REL 00000000 000bec 000008 08 23 3 4 [ 5] .bss NOBITS 00000000 000090 000008 00 WA 0 0 4 [ 6] .debug_abbrev PROGBITS 00000000 000090 00008c 00 0 0 1 [ 7] .debug_info PROGBITS 00000000 00011c 000111 00 0 0 1 [ 8] .rel.debug_info REL 00000000 000bf4 000118 08 23 7 4 [ 9] .debug_line PROGBITS 00000000 00022d 000045 00 0 0 1 [10] .rel.debug_line REL 00000000 000d0c 000008 08 23 9 4 [11] .rodata PROGBITS 00000000 000272 000007 00 A 0 0 1 [12] .debug_frame PROGBITS 00000000 00027c 000060 00 0 0 4 [13] .rel.debug_frame REL 00000000 000d14 000030 08 23 12 4 [14] .debug_loc PROGBITS 00000000 0002dc 000084 00 0 0 1 [15] .debug_pubnames PROGBITS 00000000 000360 00005a 00 0 0 1 [16] .rel.debug_pubnam REL 00000000 000d44 000008 08 23 15 4 [17] .debug_aranges PROGBITS 00000000 0003ba 000020 00 0 0 1 [18] .rel.debug_arange REL 00000000 000d4c 000010 08 23 17 4 [19] .debug_str PROGBITS 00000000 0003da 0000df 01 MS 0 0 1 [20] .comment PROGBITS 00000000 0004b9 00002d 00 0 0 1 [21] .note.GNU-stack PROGBITS 00000000 0004e6 000000 00 0 0 1 [22] .shstrtab STRTAB 00000000 0004e6 0000d1 00 0 0 1 [23] .symtab SYMTAB 00000000 0009a0 0001a0 10 24 20 4 [24] .strtab STRTAB 00000000 000b40 000094 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
可执行文件
程序头部表(Program Header table)是一个以Elf32_Phdr结构体为元素的数组,定义了要被映射的段。为了加快映射的速度,可执行文件将类似的可加载区段合并为一个段。可执行文件通常只有少数几种段,例如可读可执行的代码段,可读可写的数据段,只读的只读数据段。
字段
说明
p_type
段的类型。LOAD、DYNAMIC、INTERP等。
p_offset
段在文件中的偏移。
p_vaddr
段在进程虚拟地址空间的起始地址。
p_paddr
物理装载地址。
p_filesz
段在文件中所占空间的大小。
p_memsz
段在虚拟地址空间中所占用的长度。
p_flags
段的权限,RWX。
p_align
对齐,2的指数幂。
程序头部表示例
可以使用指令readelf -l execfile查看程序头表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 [pk@localhost tiny_hello]$ readelf -l tiny_hello Elf file type is EXEC (Executable file) Entry point 0x80480c4 There are 3 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align LOAD 0x000000 0x08048000 0x08048000 0x000e4 0x000e4 R E 0x1000 LOAD 0x0000e4 0x080490e4 0x080490e4 0x00010 0x0001c RW 0x1000 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4 Section to Segment mapping: Segment Sections... 00 .text .rodata 01 .data .bss 02
ELF共享目标文件
ELF第三种文件类型是共享目标文件,它包含了可重定位文件和可执行文件的所有东西。也就是说,共享目标文件既可以参与链接,也可以被加载到内容中执行。除此之外,共享目标文件还有一些独有的段,例如.got、.plt、.interp等。
常见区段
常见区段以及查看区段数据的指令。
符号表
符号表存储了ELF文件中每个符号的相关信息,是一个非常重要的表。通过符号表,可以得到非常多有用的信息,是分析ELF文件的基础。符号表是Elf32_Sym结构体数组,段名一般叫.symtab,每个表项定义了一个符号。
st_name:符号名,字符串表.strtab的下标。
st_size:符号大小。
st_shndx:符号所在的段。
符号所在的段在段表中的下标。
SHN_ABS:该符号包含了一个绝对的值,例如文件名的符号。
SHN_COMMON:该符号是一个“COMMON块”类型的符号,例如未初始化的全局符号定义。
SHN_UNDEF:符号未定义,该符号在本文件中被引用,但是定义在其他文件。
st_info:符号类型和绑定信息
高28位表示符号绑定信息(Symbol Binding)
STB_LOCAL:局部符号,对目标文件的外部不可见。
STB_GLOBAL:全局符号,外部可见。
STB_WEAK:若引用。
低4位表示符号的类型(Symbol Type)
STT_NOTYPE:未知类型符号
STT_OBJECT:该符号是一个数据对象,比如变量、数组。
STT_FUNC:该符号是函数或其他可执行代码。
STT_SECTION:该符号是一个段,一定是STB_LOCAL。
STT_FILE:目标文件对应的源文件名,一定是STB_LOCAL,st_shndx一定是SHN_ABS。
st_value:符号值
目标文件
SHN_UNDEF:st_value没有用。
SHN_COMMON:表示该符号的对齐属性。
段的下标:st_value表示该符号在段中的偏移位置。
可执行文件
符号表示例
可以使用指令readelf -s elffile查看符号表中的每一个表项。输出的第一列(Num)是符号在符号表的索引,第二列(Value)是符号值,第三列(Size)是符号大小,第四列(Type)是符号类型,第五列(Bind)是绑定类型,第六列(Vis)是??,第七列是(Ndx)是符号所在的段,第八列(Name)是符号名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 [pk@localhost tiny_hello]$ readelf -s tiny_hello Symbol table '.symtab' contains 28 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 08048094 0 SECTION LOCAL DEFAULT 1 2: 080480dd 0 SECTION LOCAL DEFAULT 2 3: 080490e4 0 SECTION LOCAL DEFAULT 3 4: 080490f4 0 SECTION LOCAL DEFAULT 4 5: 00000000 0 SECTION LOCAL DEFAULT 5 6: 00000000 0 SECTION LOCAL DEFAULT 6 7: 00000000 0 SECTION LOCAL DEFAULT 7 8: 00000000 0 SECTION LOCAL DEFAULT 8 9: 00000000 0 SECTION LOCAL DEFAULT 9 10: 00000000 0 SECTION LOCAL DEFAULT 10 11: 00000000 0 SECTION LOCAL DEFAULT 11 12: 00000000 0 SECTION LOCAL DEFAULT 12 13: 00000000 0 SECTION LOCAL DEFAULT 13 14: 00000000 0 FILE LOCAL DEFAULT ABS tiny_hello.c 15: 080490e8 4 OBJECT LOCAL DEFAULT 3 global_static_init 16: 080490f0 4 OBJECT LOCAL DEFAULT 3 local_static_init.763 17: 080490f4 4 OBJECT LOCAL DEFAULT 4 local_static_uninit.762 18: 080490f8 4 OBJECT LOCAL DEFAULT 4 global_static_uninit 19: 08048094 31 FUNC GLOBAL DEFAULT 1 print 20: 080490e4 4 OBJECT GLOBAL DEFAULT 3 global_init 21: 080480c4 25 FUNC GLOBAL DEFAULT 1 nomain 22: 080490fc 4 OBJECT GLOBAL DEFAULT 4 global_uninit 23: 080490f4 0 NOTYPE GLOBAL DEFAULT ABS __bss_start 24: 080490f4 0 NOTYPE GLOBAL DEFAULT ABS _edata 25: 08049100 0 NOTYPE GLOBAL DEFAULT ABS _end 26: 080490ec 4 OBJECT GLOBAL DEFAULT 3 str 27: 080480b3 17 FUNC GLOBAL DEFAULT 1 exit
代码段
代码段存储了机器码,常见的代码段有.text、.init、.fini等。可以通过指令objdump -d elffile查看反汇编之后的代码,选项-j section_name可用于反汇编指定段。objdump -S elffile同时显示C代码和反汇编代码,这需要使用-g参数编译。objdump -s elffile -j .text查看.text段的十六进制内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 [pk@localhost tiny_hello]$ objdump -d tiny_hello -j .text tiny_hello: file format elf32-i386 Disassembly of section .text: 08048094 <print>: 8048094: 55 push %ebp 8048095: 89 e5 mov %esp,%ebp 8048097: 53 push %ebx 8048098: a1 ec 90 04 08 mov 0x80490ec,%eax 804809d: ba 06 00 00 00 mov $0x6,%edx 80480a2: 89 c1 mov %eax,%ecx 80480a4: bb 01 00 00 00 mov $0x1,%ebx 80480a9: b8 04 00 00 00 mov $0x4,%eax 80480ae: cd 80 int $0x80 80480b0: 5b pop %ebx 80480b1: 5d pop %ebp 80480b2: c3 ret 省略exit和nomain函数的反汇编代码......
有时候只需要查看某个函数的反汇编代码,而不是段中所有函数的反汇编代码。这可以通过gdb来完成。
1 gdb /bin/ls -batch -ex 'disassemble main'
数据段
数据段存储了全局或静态变量的初始值,常见的数据段有.data和.rodata,.data存储可读写的数据,.rodata存储只读数据。数据段只是简单的将各个变量的初始值罗列在一起,所以只能直接查看其二进制的值。还有一个特殊的数据段是.bss,在可执行文件中不占用空间,只在虚拟内存中占用空间,在.bss段中的变量,初始值都是0。可以使用指令objdump -s elffile -j .data查看数据段的十六进制内容。
1 2 3 4 5 6 7 8 [pk@localhost tiny_hello]$ objdump -s tiny_hello -j .data -j .rodata tiny_hello: file format elf32-i386 Contents of section .rodata: 80480dd 68656c6c 6f0a00 hello.. Contents of section .data: 80490e4 02000000 03000000 dd800408 05000000 ................
字符串表
字符串存储了变量、函数、段名的字符串。一般情况下,.strtab存储了变量和函数的名称,.shstrtab存储了各个段的名称,.dnystr存储了需要动态链接的变量和函数的名称,是.strtab的子集,且只在动态库文件中存在。可以使用指令readelf elffile -p section查看字符串表中的内容。字符串前方的数字是字符串第一个字符在字符串表中的偏移。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 [pk@localhost tiny_hello]$ readelf tiny_hello -p .strtab -p .shstrtab String dump of section '.shstrtab': [ 1] .symtab [ 9] .strtab [ 11] .shstrtab [ 1b] .text [ 21] .rodata [ 29] .data [ 2f] .bss [ 34] .comment [ 3d] .debug_aranges [ 4c] .debug_pubnames [ 5c] .debug_info [ 68] .debug_abbrev [ 76] .debug_line [ 82] .debug_frame [ 8f] .debug_str [ 9a] .debug_loc String dump of section '.strtab': [ 1] tiny_hello.c [ e] global_static_init [ 21] local_static_init.763 [ 37] local_static_uninit.762 [ 4f] global_static_uninit [ 64] print [ 6a] global_init [ 76] nomain [ 7d] global_uninit [ 8b] __bss_start [ 97] _edata [ 9e] _end [ a3] str [ a7] exit
ELF实例分析
分析一个ELF文件,目的是为了从中获取到一些有用的信息。由于可重定位文件、可执行文件和共享库文件有不同的特点,所以关注的侧重点也不同。对于可重定位和共享库文件来说,可以参与链接,所以比较关心它们的导出、导入符号。对于可执行文件和共享库文件来说,还可以被加载到内存中运行,比较关心加载地址、(虚拟)内存布局、依赖的动态库(动态链接)。
以tiny_hello为例,简单分析一下ELF可执行文件。首先从文件头可以看到程序的入口地址,这是程序开始运行的地方。通过section headers的入口地址、大小以及数量,可以找到其余所有区段的位置。通过program headers的入口地址、大小及数量,可以确定所有可加载段的位置。通过shstrtab的索引,可以确定每个区段的名字。
1 2 3 4 5 6 7 8 9 10 [pk@localhost tiny_hello]$ readelf -h tiny_hello ...... Entry point address: 0x80480c4 Start of program headers: 52 (bytes into file) Start of section headers: 1512 (bytes into file) Size of program headers: 32 (bytes) Number of program headers: 3 Size of section headers: 40 (bytes) Number of section headers: 17 Section header string table index: 14
由于可执行文件是可加载的,接下来看看program headers。从表中可以看出,有两个可加载的segment。第一个segment由.text和.rodata组合而成,包含一些只读的代码和数据。第二个segment由.data和.bss组合而成,包含一些可读写的数据。首先来看看FileSiz和MemSiz,分别表示在文件中占用的空间和在内存中占用的空间。第一个segment两者一致,第二个Segment的MemSiz稍大一些,这是因为.bss在文件中不占用空间,只占用内存中的空间。接下来分析VirtAddr和PhyAddr,分别表示虚拟地址和物理地址。一般情况下,两者的大小是一致的,在一些特殊的情况,比如bootloader、kernel、单片机程序中,两者可能不一致。
为什么第二个segment的加载地址,是从0x080490e4开始,而不是0x08049000开始呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 [pk@localhost tiny_hello]$ readelf tiny_hello -l There are 3 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align LOAD 0x000000 0x08048000 0x08048000 0x000e4 0x000e4 R E 0x1000 LOAD 0x0000e4 0x080490e4 0x080490e4 0x00010 0x0001c RW 0x1000 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4 Section to Segment mapping: Segment Sections... 00 .text .rodata 01 .data .bss 02
为了弄清楚内存中的内容,需要看看符号表中的内容。下面的符号表已将不需要的内容删除了,并重新排序了。有三个类型为FUNC的符号,均在区段1,即.text区段。而且全部都是GLOBAL类型的符号,说明从外部是可见的。
重点在7个类型为OBJECT的符号。带有static的符号,均是LOCAL;不带static的符号均是GLOBAL。带有uninit的符号,均在区段4,即.bss区段;带有init的符号,均在区段3,即.data区段。对比源代码可以看出,nomain()定义的local_init和local_uninit变量没有出现到符号表中,因为这两个变量运行时在栈中分配空间,所以符号表中没有。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [pk@localhost tiny_hello]$ readelf tiny_hello -s Symbol table '.symtab' contains 28 entries: Num: Value Size Type Bind Vis Ndx Name 21: 080480c4 25 FUNC GLOBAL DEFAULT 1 nomain 19: 08048094 31 FUNC GLOBAL DEFAULT 1 print 27: 080480b3 17 FUNC GLOBAL DEFAULT 1 exit 15: 080490e8 4 OBJECT LOCAL DEFAULT 3 global_static_init 16: 080490f0 4 OBJECT LOCAL DEFAULT 3 local_static_init.763 17: 080490f4 4 OBJECT LOCAL DEFAULT 4 local_static_uninit.762 18: 080490f8 4 OBJECT LOCAL DEFAULT 4 global_static_uninit 20: 080490e4 4 OBJECT GLOBAL DEFAULT 3 global_init 22: 080490fc 4 OBJECT GLOBAL DEFAULT 4 global_uninit 26: 080490ec 4 OBJECT GLOBAL DEFAULT 3 str
1 2 3 4 5 6 7 8 9 080480c4 <nomain>: 80480c4: 55 push %ebp 80480c5: 89 e5 mov %esp,%ebp 80480c7: 83 ec 10 sub $0x10,%esp # 栈指针减少0x10,为了4个int型变量预留栈空间 80480ca: c7 45 fc 04 00 00 00 movl $0x4,-0x4(%ebp) # 赋值为4 80480d1: e8 be ff ff ff call 8048094 <print> 80480d6: e8 d8 ff ff ff call 80480b3 <exit> 80480db: c9 leave 80480dc: c3 ret
对符号表的分析可以看出,有4个变量定义在了.data段,来看看.data段中的内容。.data段从0x080490e4开始,数据段中的值可以与符号表对上。
1 2 3 4 5 6 [pk@localhost tiny_hello]$ objdump tiny_hello -s -j .data tiny_hello: file format elf32-i386 Contents of section .data: 80490e4 02000000 03000000 dd800408 05000000 ................
以上分析了ELF可执行文件的入口地址、内存布局以及如何查看数据段的内容。
小结
本文以tiny_hello.c编译出来的目标文件和可执行文件为例子,利用objdump和readelf工具,分析了ELF文件的格式,并对常见区段的内容进行了重点讲解。然而,以上没有包含ELF格式的所有内容,还有重定位表、.got、.plt和.interp等内容没有介绍,这些内容会和链接、动态库一起介绍。