[toc]
上篇文章我们从整体上介绍了从C文件到可执行文件的编译过程,并逐个分析了单步编译时生成的中间文件的类型。为了搞清楚编译和链接过程中主要做了哪些工作, 我们应该首先明白编译前后,链接前后文件内容的改变。根据上篇文章内容,编译前的文件格式是汇编文件
,编译后的文件是可重定位文件
。汇编文件就是简单的文本文件,而可重定位文件是一个ELF格式的二进制文件。因此,本章我们将从ELF文件格式入手分析可重定位文件
的结构。
一、生成中间文件
我们依然使用计算机原理系列之三 ——– 如何编译目标文件中的hello.c文件来研究,使用下列命令生成hello.o
:
gcc -E hello.c -o hello.i
gcc -S hello.i -o hello.s
gcc -c hello.s -o hello.o
二、可重定位文件分析
2.1 解析文件头,说明文件构成
由于可重定位文件类型是ELF格式(关于ELF文件的知识,请阅读详解ELF文件),我们可以使用READELF
命令查看其构成。
$ readelf -h hello.o
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: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 672 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 13
Section header string table index: 10
其中,我们关注以下字段:
Type: REL (Relocatable file)
: 可以看出.o
文件的类型为可重定位文件
;Number of program headers: 0
:可以看出可重定位文件
的program header table
的长度为0。这是因为program header table
保存的是segment
信息,而segment
是为了给加载器提供可执行程序在加载时所需的信息的,又因为可重定位文件本身并不能直接执行,因此在可重定位文件里不需要program header table
;Entry point address: 0x0
: 同上,由于可重定位文件不能直接执行,因此其入口地址为0(默认值);*** of section headers:***
:从ELF文件起始地址偏移672
个字节处是section header table
的起始地址,section header table
中共有13
项,每项的大小为64 byte
;Size of this header: 64
: ELF文件头大小为64 byte
。
2.2 分析ELF文件各部分
可重定位文件属于二进制文件。在linux机器上,可以使用hexdump
命令来查看二进制文件的内容。
$ hexdump -C hello.o
00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............|
00000010 01 00 3e 00 01 00 00 00 00 00 00 00 00 00 00 00 |..>.............|
00000020 00 00 00 00 00 00 00 00 a0 02 00 00 00 00 00 00 |................|
00000030 00 00 00 00 40 00 00 00 00 00 40 00 0d 00 0a 00 |....@.....@.....|
00000040 55 48 89 e5 bf 00 00 00 00 e8 00 00 00 00 b8 00 |UH..............|
00000050 00 00 00 5d c3 68 65 6c 6c 6f 2c 20 77 6f 72 6c |...].hello, worl|
... snip ...
这实际上就是hello.o
文件的实际内容。
注:
1. 最前面一列是hexdump命令添加的,并非ELF文件的内容。它是一个十六进制的数字,表示字节序号。例如,00000040 55 48 89 e5 bf 00 00 00 00 e8 00 00 00 00 b8 00
,其中0000040
即0x40
,十进制为64
。表示hello.o
文件的第64
个字节是‘55’
。
2. 最后一部分由两个‘|’
包含的数字和字符也是hexdump命令添加的,它将其左侧的十六进制数字转化成了对应的ASCII字符,所有的控制字符表示为‘.’
,所有的可显示字符表示为对应的字符或图形。
2.2.1 ELF header
根据ELF文件的结构,ELF文件最开始的部分是ELF header
,它是一个64字节大小的结构体,也就是对应了hello.o
的前64个字符,即从0000000 - 00000040
的部分。前十六个字节应该是对应其magic number
的部分。我们注意到,从0000000 - 0000000F
正好就是使用readelf读出来的magic的值。剩下的部分只要结合struct ElfN_Ehdr
的成员信息和hexdump
命令输出的内容即可一一验证。
2.2.2 section headers table及sections
由于ELF文件头中说明了在该可重定位文件中不存在program header table
,因此,根据ELF文件的结构,接下来就应该是各个section了。section的信息是由section header table
描述的,我们可以通过以下命令查看section header table
:
$ readelf -S hello.o
There are 13 section headers, starting at offset 0x2a0:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000015 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 000001f0
0000000000000030 0000000000000018 I 11 1 8
[ 3] .data PROGBITS 0000000000000000 00000055
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 00000055
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .rodata PROGBITS 0000000000000000 00000055
000000000000000d 0000000000000000 A 0 0 1
[ 6] .comment PROGBITS 0000000000000000 00000062
0000000000000036 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 00000098
0000000000000000 0000000000000000 0 0 1
[ 8] .eh_frame PROGBITS 0000000000000000 00000098
0000000000000038 0000000000000000 A 0 0 8
[ 9] .rela.eh_frame RELA 0000000000000000 00000220
0000000000000018 0000000000000018 I 11 8 8
[10] .shstrtab STRTAB 0000000000000000 00000238
0000000000000061 0000000000000000 0 0 1
[11] .symtab SYMTAB 0000000000000000 000000d0
0000000000000108 0000000000000018 12 9 8
[12] .strtab STRTAB 0000000000000000 000001d8
0000000000000013 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文件一共有13个section header table
,具体分布如下:
图1. 可重定位文件结构
各个section的详细内容如下:
.text
段保存了可执行代码经过汇编之后的内容,后面的文章我们会详细介绍;.data
和.bss
并没有占据任何空间,原因是代码中并未定义局部变量或者全局变量;.rodata
的起始地址是0x55
,占据0xd个字节的空间,根据hexdump
命令输出的信息,其内容应该是:68 65 6c 6c 6f 2c 20 77 6f 72 6c 64 00
,根据ASCII表,其对应的字符为:h e l l o , w o r l d NULL
。正好就是我们的C代码中唯一的一个需要保存到.rodata
段的字符串常量”hello, world”;.commont
的起始地址是0x62
,大小为0x36 byte
,将hexdump内容截取出来,并转化成ASCII字符为:GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609
,可以看出正好就是我们在前面介绍开发环境的时候使用的GCC版本信息;.strtab
指的是string table
,起始地址是0x1d8
,大小为0x13 byte
,将hexdump内容截取出来,并转化成ASCII字符为:hello.c main puts
,可见其中保存了原C文件中的文件名和函数名等信息;.shstrtab
指的是section header string table
,经过同上分析,其中保存了各个section的名字;.symtab
保存了符号表,其中包括了.strtab
里面定义的三个符号;每个符号对应的符号表是一个Elf64_Symbol
结构体,详细信息参考参考阅读[2]。除了包含.strtab
外,符号表中还包含了一些section的符号表条目,这些条目给链接的时候需要和其他可重定位文件或者库的对应的section合并时提供了必要的信息。在上述例子中,其.symtab
的内容如下:
$ readelf -s hello.o
Symbol table '.symtab' contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000000 0 SECTION LOCAL DEFAULT 6
9: 0000000000000000 21 FUNC GLOBAL DEFAULT 1 main
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
.eh_frame
:其内部存放了以DWARF格式
保存的一些调试信息。关于eh_frame详细分析,可以查看参考阅读[3]和[4].rel.text
:包含了代码段中引用的外部函数和全局变量的重定位条目。上述hello.o
文件中我们得到它的.rel.text
的内容包括:
Relocation section '.rela.text' at offset 0x1f0 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000005 00050000000a R_X86_64_32 0000000000000000 .rodata + 0
00000000000a 000a00000002 R_X86_64_PC32 0000000000000000 puts - 4
.rela.eh_frame
:这个section同.rel.text
一样属于重定位信息的section,只不过它包含的是eh_frame
的重定位信息,其内容可以参考阅读[3]
注:各个段之间并未严格的首位相接,考虑到对齐的因素,他们之间会存在”空洞”(如图中.shstrtab段和Section Header Table之间的padding分区);
至此,我们可以看到经过编译之后,人类可以理解的汇编文件,变成了上图所示的计算机可以理解的可重定位文件。
三、参考阅读
- SYSTEM V APPLICATION BINARY INTERFACE
- Symbol Table
- eh_frame相关:
1) c++ 异常处理(2)
2) Linux Standard Base Core Specification 3.0RC1
3) CFI directives in assembly files - DWARF相关:
1) DWARF, 说不定你也需要它哦
2) Introduction to the DWARF Debugging Format - 实例分析ELF文件静态链接
- http://oiwjee823.bkt.clouddn.com/ELF%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F%E5%88%86%E6%9E%90.pdf
- ELF 格式解析