编译器在完成阶段7和阶段8后,会生成目标文件。在Linux平台上,目标文件的格式为 ELF (executable linkable file)。ELF也是动态库、静态库、可执行文件和core dump文件的格式。本文会介绍:
- ELF文件的结构:
- ELF Header,
- Section header table,
- String Table
- Symbol Table
- Relocation Table
- 局部符号LOCAL,全局符号GLOBAL和弱全局符号WEAK
目标文件是 ELF 文件
1 | // elf.cpp |
使用g++ -c elf.cpp
编译上面的文件之后,我们就可以得到elf.o
这个目标文件。使用file
来查看它的格式:
1 | $ file elf.o |
其中:
- ELF 64bit:表示其为64位的executable linkable file
- relocatable:表示该文件还未进行重定位,重定位将会在翻译阶段9进行。
- LSB:表示小端序,即least significant byte。大端序为MSB,即most significant byte
- x86-64:ELF头的
e_machine
字段 - version 1 (SYSV):ELF头的
e_ident[EI_OSABI]
表示Operating System and ABI. - not stripped: 使用strip之后可以去除ELF文件的某些section.
用file
来查看可执行文件/bin/bash
、g++-9
和OpenGL的动态库的格式
1 | $ file /bin/bash |
1 | $ file /usr/bin/x86_64-linux-gnu-g++-9 |
1 | $ file /usr/lib/x86_64-linux-gnu/libGL.so.1.7.0 |
同为可执行文件,为什么
/bin/bash
和OpenGL动态库都是shared object
,而gcc-9
是executable
?shared object
可以是一个动态库,也可以是一个可执行文件。当shared object
是一个可执行文件时,其是一个PIE(positional independent executable)的可执行文件。因此操作系统可以对其进行ASLR(address space layout randomization)来提高程序的安全性,防范类似ROP这样的利用buffer溢出的攻击。
ELF 文件的结构
1 | a relocatable ELF shared object or executable |
ELF Header
ELF 文件头的结构定义于/usr/include/elf.h
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf64_Half e_type; /* Object file type */
Elf64_Half e_machine; /* Architecture */
Elf64_Word e_version; /* Object file version */
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags; /* Processor-specific flags */
Elf64_Half e_ehsize; /* ELF header size in bytes */
Elf64_Half e_phentsize; /* Program header table entry size */
Elf64_Half e_phnum; /* Program header table entry count */
Elf64_Half e_shentsize; /* Section header table entry size */
Elf64_Half e_shnum; /* Section header table entry count */
Elf64_Half e_shstrndx; /* Section header string table index */
} Elf64_Ehdr;
我们使用readelf -h elf.o
来查看我们编译生成的目标文件的 ELF 文件头:
1 | ELF Header: |
- e_ident是一个长度为16的
unsigned char
数组,正好对应Magic到ABI Version这六个词条。
1 | +------------+ |
- e_ident的前四个字节为Magic。Magic帮助操作系统用来识别文件格式。比如:
- ELF的Magic为
7f 45 4c 46
- PNG图片的Magic为
89 50 4E 47 0D 0A 1A 0A
,其中50 4E 47
为'P' 'N' 'G'
的ASCII码。 - PDF的Magic为
25 50 44 46 2d
,其中50 44 46
为'P' 'D' 'F'
的ASCII码 - File Version 为 ELF文件的版本号,常设置为
01
- PNG图片的Magic为
- OS/ABI 常常设置为
00
,来表示该ELF没有用到和平台相关的功能。 - ABI Version:如果OS/ABI被设置为
ELFOSABI_GNU
(即3),则该字节被认为dynamic linker的ABI version。- Linux中
/usr/include/elf.h
定义ELFOSABI_GNU
和ELFOSABI_LINUX
相同 - sourceware.org Git - glibc.git/blob - libc-abis
- Linux中
- 后7个字节未定义,以
00
填充。
- ELF的Magic为
- Type 对应
e_type
,用来表明该 ELF 的类型,比如这里是一个ET_REL
,即ELF relocatable。ET_EXEC
则对应EFL executable,等等。 - Machine 对应
e_machine
,说明该 ELF 使用的指令集架构 - Flags 的解释是平台相关的
- 其他部分则和Program header table和Section header table相关。
Section Header Table
ELF header 中 Start of section headers 和 Size of section headers 决定了段表(section header table)的开始位置和大小。段表记录了ELF 文件中所有的section的名字、偏移量、长度、读写权限等。段表中记录每个段的数据结构叫做Section Descriptor,段表则是一个Section Descriptor的数组。Elf64_Shdr
定义在/usr/inclde/elf.h
1 | typedef struct |
使用readelf -S elf.o
来读取之前生成的目标文件的段表: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
37
38
39
40
41
42Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .group GROUP 0000000000000000 00000040
000000000000000c 0000000000000004 14 20 4
[ 2] .text PROGBITS 0000000000000000 0000004c
000000000000003b 0000000000000000 AX 0 0 1
[ 3] .rela.text RELA 0000000000000000 00000478
0000000000000060 0000000000000018 I 14 2 8
[ 4] .data PROGBITS 0000000000000000 00000088
0000000000000008 0000000000000000 WA 0 0 4
[ 5] .bss NOBITS 0000000000000000 00000090
000000000000000c 0000000000000000 WA 0 0 4
[ 6] .rodata PROGBITS 0000000000000000 00000090
000000000000000b 0000000000000000 A 0 0 1
[ 7] .text._Z24global_ PROGBITS 0000000000000000 0000009b
0000000000000021 0000000000000000 AXG 0 0 1
[ 8] .rela.text._Z24gl RELA 0000000000000000 000004d8
0000000000000030 0000000000000018 IG 14 7 8
[ 9] .comment PROGBITS 0000000000000000 000000bc
0000000000000025 0000000000000001 MS 0 0 1
[10] .note.GNU-stack PROGBITS 0000000000000000 000000e1
0000000000000000 0000000000000000 0 0 1
[11] .note.gnu.propert NOTE 0000000000000000 000000e8
0000000000000020 0000000000000000 A 0 0 8
[12] .eh_frame PROGBITS 0000000000000000 00000108
0000000000000078 0000000000000000 A 0 0 8
[13] .rela.eh_frame RELA 0000000000000000 00000508
0000000000000048 0000000000000018 I 14 12 8
[14] .symtab SYMTAB 0000000000000000 00000180
0000000000000228 0000000000000018 15 14 8
[15] .strtab STRTAB 0000000000000000 000003a8
00000000000000ca 0000000000000000 0 0 1
[16] .shstrtab STRTAB 0000000000000000 00000550
00000000000000a9 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
其中 Offset(sh_offset
) 和 Size (sh_size
) 被用来确定段在ELF 文件中的位置。
Type(sh_type
)和Flag(sh_flags
)来决定的段的属性。
- NULL:段表的数组的第一个元素为空。
- GROUP :section group,在编译器的greedy instantiation后,用来对模板进行去重
- PROGBITS :程序段、代码段、数据段等
- RELA :重定位表,用来进行重定位,见静态链接。
- NOTE :提示信息等
- SYMTAB :符号表
- STRTAB :字符串表
Relocation Table
使用objdump -d elf.o
反汇编之前的段表中带X
(可执行标志位)的段
1 | Disassembly of .text: |
注意到其中所有的函数调用
1 | f: e8 00 00 00 00 callq 14 <_Z15global_functionv+0x14> |
1 | 2a: e8 00 00 00 00 callq 2f <main+0x18> |
1 | 19: e8 00 00 00 00 callq 1e <_Z24global_function_templateImEDav+0x1e> |
callq
指令后的地址是相对于其下一条指令的偏移量。显然这里所有callq
指令都处于一个还未填充的状态,因此需要重定位。不仅仅代码段需要重定位,每个有绝对地址引用的段都需要进行重定位。重定位的信息就保存在重定位表中。关于重定位表,具体在静态链接中讲解。
String Table
ELF 文件中的段名、符号名等,都被保存在字符串表中。.shstrtab
是段表字符串表。.strtab
为字符串表。字符串表中的字符串都是'\0'
结尾的,因此ELF 文件可以使用字符串在字符串表中的偏移来表示字符串。
使用readelf --string-dump .shstrtab elf.o
来查看段表字符串表1
2
3
4
5
6
7
8
9
10
11
12
13
14String dump of section '.shstrtab':
[ 1] .symtab
[ 9] .strtab
[ 11] .shstrtab
[ 1b] .rela.text
[ 26] .data
[ 2c] .bss
[ 31] .rodata
[ 39] .rela.text._Z24global_function_templateImEDav
[ 67] .comment
[ 70] .note.GNU-stack
[ 80] .note.gnu.property
[ 93] .rela.eh_frame
[ a2] .group
使用readelf --string-dump .strtab elf.o
来查看字符串表1
2
3
4
5
6
7
8
9
10
11
12
13String dump of section '.strtab':
[ 1] elf.cpp
[ 9] _ZZ4mainE15static_not_init
[ 24] _ZZ4mainE11static_init
[ 3b] global_not_init
[ 4b] global_init
[ 57] global_init_as_zero
[ 6b] _Z15global_functionv
[ 80] _GLOBAL_OFFSET_TABLE_
[ 96] puts
[ 9b] _Z24global_function_templateImEDav
[ be] printf
[ c5] main
Symbol Table
在链接过程中,我们把函数和变量都看作符号(symbol)每一个函数和变量都有一个自己的符号名。链接的过程是基于符号完成的。符号有符号名和符号值。符号值为符号的地址。符号主要分为以下几类:
- 定义在本目标文件的全局符号,可以被其他目标文件引用
- 外部符号(External Symbol):在本目标文件中引用的全局符号,但没有定义在本目标文件
- 段名
- 局部符号:只在该编译单元内部可见
符号在ELF 中被组织成符号表的形式储存在.symtab
中。符号表是一个Elf64_Sym
的数组。Elf64_Sym
在/usr/include/elf.h
中的定义如下:
1 | typedef struct |
st_name
符号名,用字符串表中下标来表示st_info
符号类型与绑定信息st_other
一个符号的visibility,在gcc中可以通过__attribute((visibility("visibility_type")))__
或者[[gnu::visibility("visibility_type")]]
来进行调整。关于visibility的详细内容,将在动态链接中讲解。st_shndx
符号所在的段的indexst_value
符号值,不同的符号类型,其符号值的含义不同。st_size
符号的大小,例如一个表示double
类型数据的符号,其大小一般为8字节
st_info
的高四位可以通过下面的宏来提取。1
其高四位的可能取值为:
STB_LOCAL
局部符号。局部符号包含其定义的目标文件外部不可见。多个文件可以存在同名但不相互指涉的局部符号。STB_GLOBAL
全局符号 全局符号对所有链接过程中合并的目标文件都可见。一个文件对全局符号的定义,应满足另一个文件对同一符号的未定义引用。STB_WEAK
**弱全局符号**
其低四位的可以通过下面的宏来提取1
其低四位可能的取值为:
STT_NOTYPE
未指明符号类型STT_OBJECT
该符号和一个数据对象相关,比如一个变量或一个数组STT_FUNC
该符号和一个函数或其他可执行代码相关STT_SECTION
该符号和一个section相关。这样的符号主要和重定位相关,并拥有STB_LOCAL
的绑定。STT_FILE
按照惯例,该符号的名字是生成该目标文件的源文件名。
关于符号表的详细解释,可以参照Symbol Table - Oracle - Linker and Libraries Guide
一些符号的st_shndx
值可能不指其所在的段。
SHN_ABS
(0xfff1
):表示该符号有一个绝对的值。例如Type为STT_FILE
的符号。SHN_COMMON
(0xfff2
):该符号代表一个还未被分配空间的”common block”。该符号的value指代的是其内存对其的限制。关于”common block”,见静态链接。SHN_UNDEF
(0
): 该符号为定义。该符号在本目标文件中被引用,但是定义在其他目标文件中。
使用readelf -s elf.o
来查看符号表的内容。
1 | Symbol table '.symtab' contains 23 entries: |
puts
、printf
和_GLOBAL_OFFSET_TABLE_
所在的section(Ndx)都为UND(undefined),绑定信息为GLOBAL。这些符号在elf.o
中被引用,但是不定义在其中。_GLOBAL_OFFSET_TABLE_
的作用是对Position-Independent Code中全局函数或全局变量进行寻址。Global Offset Tablesglobal_not_init
的Ndx为5,即.bss
section。而global_init
则被放在.data
中。因为global_not_init
未初始化,因此可以直接被放置在清零的.bss
段中,节省ELF文件的空间。同样地,局部
static
变量_ZZ4mainE11static_init
和_ZZ4mainE15static_not_ini
也是同样的情况。函数模板的实例化
_Z24global_function_templ
则被单独放在.text._Z24global_
section中,这是为了以后去重方便。Type为
SECTION
的符号,其符号名和段名相同,因此被省略。
注意到模板函数void global_function_temp<size_t>
和void global_function
的名字_Z24global_function_templ
和_Z15global_functionv
都是经过C++的name mangling的名字。C不允许函数重载而C++可以进行函数重载,因此C和C++有不同name mangling规则,为了让C++兼容C的修饰机制,我们需要使用extern "C"
。单独声明某个函数或变量,可以直接将extern "C"
放在声明之前1
2
3
4
5
6extern "C" int global_function_c;
// or use braces
extern "C"
{
// ...
}
全局符号与弱全局符号
ELF中的符号BIND主要有三种:
- `ST_GLOBAL` 全局符号
- `ST_WEAK` 弱全局符号
- `ST_LOCAL` 局部符号
ST_WEAK
仍然是全局可见的符号,然而ST_WEAK
相比ST_GLOBAL
有以下区别:
- 当链接器将几个需要重定位的目标文件组合在一起时,不允许出现多个具有相同名字的
STB_GLOBAL
符号。然而,如果存在一个有定义的STB_GLOBAL
符号,其他具有相同名字的STB_WEAK
符号并不会导致链接错误。链接器将会选择拥有STB_GLOBAL
BIND的符号,而忽略STB_WEAK
BIND的符号。 - 类似地,如果存在一个有
SHN_COMMON
作为st_shndx
的符号和多个STB_WEAK
的符号,链接器会选择SHN_COMMON
的那一个。 如果一个符号在所有的目标文件中都是
STB_WEAK
,则选择占用空间最大的那个。
而在上面的例子中,我们看到函数模板的实例化_Z24global_function_templ
是一个弱符号。1
2Num: Value Size Type Bind Vis Ndx Name
20: 0000000000000000 33 FUNC WEAK DEFAULT 7 _Z24global_function_templ
因为C++的inclusion model和greedy instantiation会导致同一个模板在多个编译单元被生成多份,因此该模板的BIND不可以是ST_GLOBAL
。当多个编译单元拥有相同符号名的多个该模板的实例化时,链接器会选择其中一个。
同样地,C++中对inline
函数的处理也和模板相同。