[toc]
前面的文章我们详细的讲解了.o文件的结构及其编译过程,这篇文章我们从ELF文件的观点来分析可执行文件的结构。
一、生成可执行文件
我们以下面两个文件为例来研究多个文件生成一个可执行文件的过程。一个文件名为add.c
,主要内容包含一个add
函数,该函数功能为:将调用次数加1,然后,返回传递进来的两个参数之和。
extern int times;
int add(int n1, int n2) {
times++;
return n1 + n2;
}
另一个文件名为main.c
,其功能为:调用两次add
函数,并打印出调用次数和add
函数的返回值。
#include <stdio.h>
extern int add(int, int);
int times = 0;
int main(void)
{
int a = 2;
int b = 3;
printf("%d, The sum of a and b is %d\n", times, add(a,b));
printf("%d, The sum of a and b is %d\n", times, add(a,b));
return 0;
}
由于这次我们的主要研究对象是可执行文件,因此,使用下面的命令编译这两个文件:
gcc -c add.c -o add.o
gcc -c main.c -o main.o
gcc -o add add.o main.o
这样我们生成了链接前的两个输入文件:add.o
和main.o
,以及链接之后的可执行文件add
。其中.o
的生成在可重定位文件详解和编译过程分析已经做过类似的分析,这里就不再做过多的解释。
二、可执行文件结构
2.1 可执行文件的ELF header
下面我们开看一下生成的可执行文件add
的结构,由于可执行文件也属于ELF格式的文件,因此,按照惯例,我们依然先使用readelf
工具从文件头开始分析。
$ readelf -h add
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: 0x400430
Start of program headers: 64 (bytes into file)
Start of section headers: 6712 (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: 31
Section header string table index: 28
我们分析一下字段:
Type: EXEC (Executable file)
: 该文件是一个可执行文件;Entry point address: 0x400430
: 文件的入口地址为0x400430
,该地址是代码段的起始地址;xxx of program headers: xxx
: 该文件中共有9个program header
,每个的大小为56
字节;xxx of section headers: xxx
: 该文件中共有31个section header
,每个的大小为64
字节。
这里可以看出,经过链接之后,生成了可执行文件,该文件的结构具有了如详解ELF文件中描述的完整的ELF文件结构。
2.2 可执行文件的section
通过读ELF header的信息可以看出上述生成的可执行文件add
共有31个section
。下面我们逐个分析各个section的内容。首先,通过readelf
命令查看各个section的起始地址和大小,如下:
$ readelf -S add
There are 31 section headers, starting at offset 0x1a38:
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
0000000000000060 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400318 00000318
000000000000003f 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 0000000000400358 00000358
0000000000000008 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000400360 00000360
0000000000000020 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 0000000000400380 00000380
0000000000000018 0000000000000018 A 5 0 8
[10] .rela.plt RELA 0000000000400398 00000398
0000000000000030 0000000000000018 AI 5 24 8
[11] .init PROGBITS 00000000004003c8 000003c8
000000000000001a 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 00000000004003f0 000003f0
0000000000000030 0000000000000010 AX 0 0 16
[13] .plt.got PROGBITS 0000000000400420 00000420
0000000000000008 0000000000000000 AX 0 0 8
[14] .text PROGBITS 0000000000400430 00000430
0000000000000202 0000000000000000 AX 0 0 16
[15] .fini PROGBITS 0000000000400634 00000634
0000000000000009 0000000000000000 AX 0 0 4
[16] .rodata PROGBITS 0000000000400640 00000640
0000000000000022 0000000000000000 A 0 0 4
[17] .eh_frame_hdr PROGBITS 0000000000400664 00000664
000000000000003c 0000000000000000 A 0 0 4
[18] .eh_frame PROGBITS 00000000004006a0 000006a0
0000000000000114 0000000000000000 A 0 0 8
[19] .init_array INIT_ARRAY 0000000000600e10 00000e10
0000000000000008 0000000000000000 WA 0 0 8
[20] .fini_array FINI_ARRAY 0000000000600e18 00000e18
0000000000000008 0000000000000000 WA 0 0 8
[21] .jcr PROGBITS 0000000000600e20 00000e20
0000000000000008 0000000000000000 WA 0 0 8
[22] .dynamic DYNAMIC 0000000000600e28 00000e28
00000000000001d0 0000000000000010 WA 6 0 8
[23] .got PROGBITS 0000000000600ff8 00000ff8
0000000000000008 0000000000000008 WA 0 0 8
[24] .got.plt PROGBITS 0000000000601000 00001000
0000000000000028 0000000000000008 WA 0 0 8
[25] .data PROGBITS 0000000000601028 00001028
0000000000000010 0000000000000000 WA 0 0 8
[26] .bss NOBITS 0000000000601038 00001038
0000000000000008 0000000000000000 WA 0 0 4
[27] .comment PROGBITS 0000000000000000 00001038
0000000000000035 0000000000000001 MS 0 0 1
[28] .shstrtab STRTAB 0000000000000000 00001925
000000000000010c 0000000000000000 0 0 1
[29] .symtab SYMTAB 0000000000000000 00001070
0000000000000690 0000000000000018 30 48 8
[30] .strtab STRTAB 0000000000000000 00001700
0000000000000225 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)
.interp
: 使用可重定位文件详解里介绍的hexdump命令,得到该section的内容是"/lib64/ld-linux-x86-64.so.2"
。这是程序运行时需要用到的动态链接器的名字。关于动态链接和静态链接,我们会在后面的博客解释。.note.ABI-tag
: 这个section存放的是vendor或者编译者指定的一些信息。它是以ELF spec
里面规定的SHT_NOTE
格式存放的。(具体参考本系列文章详解ELF文件参考阅读[1]).note.gnu.build-id
: 这个段主要保存的是build id,是文件的唯一标识符,他可以保存成多种格式,比如uuid形式,MD5, SHA1等,具体参考参考阅读[1].gnu.hash
: 里面保存着一个符号hash表,所有参与动态链接的对象都必须包含一个符号hash表。其用法见参考阅读[2].dynsym
: 这个section保存与动态链接相关的导入导出符号,不包括模块内部的符号。而.symtab
则保存所有符号,包括.dynsym
中的符号。
在本例中,通过readelf
命令我们知道.dynsym
里有四个条目:
Symbol table '.dynsym' contains 4 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
.gnu.version
:.gnu.version
里的每一项都对应.dynsym
中的一个符号;每个表项的内容为:hash值、glibc版本或global/local绑定属性 。
在本例中,我们从readelf命令里得到.gnu.version
里四个条目如下:
Version symbols section '.gnu.version' contains 4 entries:
Addr: 0000000000400358 Offset: 0x000358 Link: 5 (.dynsym)
000: 0 (*local*) 2 (GLIBC_2.2.5) 2 (GLIBC_2.2.5) 0 (*local*)
从其中的内容可以知道,.gnu.version
的四个条目解释了.dynsym
与之相对应的四个条目的版本。比如,printf
这个符号对应的是GLIBC_2.2.5
这个版本中的printf
函数。
.dynstr
: 这个section保存了.dynsym
中所包含的符号的符号名。.eh_frame_hdr
:在可重定位文件详解文章中,我们了解到.eh_frame
里面存放的是在发生异常时解析函数调用栈所需要的信息,而.eh_frame_hdr
section保存了.eh_frame
对应的header table。这些header table可以在运行时通过调用dl_iterate_phdr
函数方便的找到所有的PT_GNU_EH_FRAME segments
。.init
和.init_array
: 这个section中保存了该可执行程序main函数执行之前的初始化代码, 比如设置环境变量,给main函数传递参数等。关于.init
和.init_array
的关系,我们会在稍后的博客中解释。.fini
和.fini_array
: 这个section中保存了该可执行程序main函数正常退出之后执行的代码。.dynamic
: 这个section里保存了动态链接器所需要的基本信息,比如依赖哪些共享对象、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的地址等。它是由是由Elfxx_Dyn
(Elf32_Dyn
或者Elf64_Dyn
)组成的数组。Elfxx_Dyn
结构由一个类型值加上一个附加的数值或指针,对于不同的类型,后面附加的数值或者指针有着不同的含义。
.plt
和.plt.got
:plt全名(Procedure Linkage Table)保存了一组16字节代码,用于在动态链接中生成所有外部过程调用符号。.got
和.got.plt
:got全名(Global Offset Table)保存了一个数组,用于配合PLT生成库函数的真正地址并保存该地址。稍后博客中我们会详细解释该过程。.rela.plt
:这个section保存了.plt
中保存的符号的重定位信息。.rela.dyn
:这个section保存了除了.plt
中保存的符号外的其他符号的重定位信息。.jcr
:这个section保存了编译java类所必须的信息。它里面内容是编译器指定的,并且被编译器的初始化函数使用。.text
:这个section用来保存可执行程序的代码编译生成的二进制代码。其内容我们通过objdump -C add
命令查看后发现,反编译出来的函数不仅包括我们定义的add函数和main函数还包括了其他一些函数,这些函数是链接器加进去的,稍后的博客会详细解释。-
.rodata
,.eh_frame
,.data
,.bss
,.comment
,.shstrtab
,.symtab
,.strtab
和可重定位文件详解包含内容的意义是一样的,这里就不再重复介绍。
2.3 可执行文件的segment
前面我们谈到过,segment其实是由section组成的,那么哪些section组成了哪些segment呢?我们可以从readelf -l add
输出的program header table
中得到这些信息。
$ readelf -l add
Elf file type is EXEC (Executable file)
Entry point 0x400430
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
0x00000000000007b4 0x00000000000007b4 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 0x0000000000000664 0x0000000000400664 0x0000000000400664
0x000000000000003c 0x000000000000003c 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 .plt.got .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
根据上述信息:
1. 一共生成了9个segment,其中第0个segment对应PHDR,第1个segment对应INTERP,第2个segment对应LOAD,以此类推,第8个segment对应GNU_RELRO。
2. 第0个segment(PHDR)保存了各个segment的VirtAddr和MemSiz。
3. 第1个segment是只读的,只保存了.interp
section。
4. 第2个segment就是我们常说的一个可执行文件的代码段,这部分是要由加载器加载到内存中执行的,因此其属性是可读可执行的,保存了.interp .note.ABI-tag .note.gnu.build-id ...
等18个section。
5. 第3个segment是我们常说的数据段,它保存了和可执行程序执行过程相关的所有数据,这部分也是要加载到内存中的,因此其属性是可读可写的,保存了.init_array .fini_array ...
等8个section。
6. 第4个segment是可读可写的,保存了.dynamic
section。
7. 第5个segment是只读的,保存了.note.ABI-tag .note.gnu.build-id
这两个section。
8. 第6个segment是只读的,保存了.eh_frame_hdr
section。
9. 第7个segment是可读可写的,它告诉系统,当加载这个可执行文件的时候,如何设置其栈。
10. 第8个segment是只读的,保存了.init_array .fini_array .jcr .dynamic .got
这5个section。