0%

ELF文件格式

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.otiny_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_shoffe_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表示该符号在段中的偏移位置。
    • 可执行文件
      • 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组合而成,包含一些可读写的数据。首先来看看FileSizMemSiz,分别表示在文件中占用的空间和在内存中占用的空间。第一个segment两者一致,第二个SegmentMemSiz稍大一些,这是因为.bss在文件中不占用空间,只占用内存中的空间。接下来分析VirtAddrPhyAddr,分别表示虚拟地址和物理地址。一般情况下,两者的大小是一致的,在一些特殊的情况,比如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_initlocal_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编译出来的目标文件和可执行文件为例子,利用objdumpreadelf工具,分析了ELF文件的格式,并对常见区段的内容进行了重点讲解。然而,以上没有包含ELF格式的所有内容,还有重定位表、.got.plt.interp等内容没有介绍,这些内容会和链接、动态库一起介绍。