0%

C++从源文件到可执行文件:目标文件

编译器在完成阶段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
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
// elf.cpp
#include <cstdio>

int global_not_init;
int global_init = 42;
int global_init_as_zero = 0;

auto global_function()
{
puts("你好");
}

template<typename T>
auto global_function_template()
{
printf("%lu", sizeof(T));
}

int main()
{
static int static_not_init;
static int static_init = 42;

int local_init = 0;
int local_not_init;

global_function();
global_function_template<size_t>();

return 0;
}

使用g++ -c elf.cpp编译上面的文件之后,我们就可以得到elf.o这个目标文件。使用file来查看它的格式:

1
2
$ file elf.o
elf.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

其中:

  • 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/bashg++-9和OpenGL的动态库的格式

1
2
3
4
5
$ file /bin/bash

/bin/bash: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]
=a43fec47192ff49c2d3fed671f2be8df7e83784a, for GNU/Linux 3.2.0, stripped
1
2
3
4
5
6
$ file /usr/bin/x86_64-linux-gnu-g++-9

/usr/bin/x86_64-linux-gnu-g++-9: ELF 64-bit LSB executable, x86-64,
version 1 (SYSV), dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]
=5ad6c47bea7dfb6722269e2d89b5c2c5a63964d4, for GNU/Linux 3.2.0, stripped
1
2
3
$ file /usr/lib/x86_64-linux-gnu/libGL.so.1.7.0
/usr/lib/x86_64-linux-gnu/libGL.so.1.7.0:
ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, stripped

同为可执行文件,为什么/bin/bash和OpenGL动态库都是shared object,而gcc-9executable
shared object可以是一个动态库,也可以是一个可执行文件。当shared object是一个可执行文件时,其是一个PIE(positional independent executable)的可执行文件。因此操作系统可以对其进行ASLR(address space layout randomization)来提高程序的安全性,防范类似ROP这样的利用buffer溢出的攻击。

ELF 文件的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
a relocatable ELF        shared object or executable
+---------------------+ +---------------------+
|ELF Header | |ELF Header |
+---------------------+ +---------------------+
| | |Program header table |
|Sections | +---------------------+
| . | | . |
| . | | . |
| . | | . |
| | | |
| | | |
| | | |
+---------------------+ +---------------------+
|Section header Table | |Section header Table |
+---------------------+ +---------------------+
|String Table | |String Table |
+---------------------+ +---------------------+
|Symbol Table | |Symbol Table |
+---------------------+ +---------------------+

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
#define EI_NIDENT (16)

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
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 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: 1536 (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: 17
Section header string table index: 16

  • e_ident是一个长度为16的unsigned char数组,正好对应Magic到ABI Version这六个词条。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
                    +------------+
|00 invalid |
|01 LSB |
|02 MSB |
+------------+
^
ASCII 'E' 'L' 'F' | File Version
+--------+ | ^ ABI Version
| | | | ^
+---------------+--+-----+-+ +--------------------+
|7f|45|4c|46|02|01|01|00|00| |00|00|00|00|00|00|00+---->padding
++-----------+--------+----+ +--------------------+ bytes
| | |
| | v
v | OS/ABI
ASCII DEL ^
+------------+
|00 invalid |
|01 ELF 32bit|
|02 ELF 64bit|
+------------+


  • 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
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct
{
Elf64_Word sh_name; /* Section name (string tbl index) */
Elf64_Word sh_type; /* Section type */
Elf64_Xword sh_flags; /* Section flags */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Section size in bytes */
Elf64_Word sh_link; /* Link to another section */
Elf64_Word sh_info; /* Additional section information */
Elf64_Xword sh_addralign; /* Section alignment */
Elf64_Xword sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;

使用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
42
Section 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
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
Disassembly of section .text:

0000000000000000 <_Z15global_functionv>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # f <_Z15global_functionv+0xf>
f: e8 00 00 00 00 callq 14 <_Z15global_functionv+0x14>
14: 90 nop
15: 5d pop %rbp
16: c3 retq

0000000000000017 <main>:
17: f3 0f 1e fa endbr64
1b: 55 push %rbp
1c: 48 89 e5 mov %rsp,%rbp
1f: 48 83 ec 10 sub $0x10,%rsp
23: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
2a: e8 00 00 00 00 callq 2f <main+0x18>
2f: e8 00 00 00 00 callq 34 <main+0x1d>
34: b8 00 00 00 00 mov $0x0,%eax
39: c9 leaveq
3a: c3 retq

Disassembly of section .text._Z24global_function_templateImEDav:

0000000000000000 <_Z24global_function_templateImEDav>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: be 08 00 00 00 mov $0x8,%esi
d: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 14 <_Z24global_function_templateImEDav+0x14>
14: b8 00 00 00 00 mov $0x0,%eax
19: e8 00 00 00 00 callq 1e <_Z24global_function_templateImEDav+0x1e>
1e: 90 nop
1f: 5d pop %rbp
20: c3 retq

注意到其中所有的函数调用

1
f: e8 00 00 00 00        callq  14 <_Z15global_functionv+0x14>
1
2
2a: e8 00 00 00 00        callq  2f <main+0x18>
2f: e8 00 00 00 00 callq 34 <main+0x1d>
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
14
String 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
13
String 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
2
3
4
5
6
7
8
9
typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;
  • st_name 符号名,用字符串表中下标来表示
  • st_info 符号类型与绑定信息
  • st_other 一个符号的visibility,在gcc中可以通过__attribute((visibility("visibility_type")))__或者[[gnu::visibility("visibility_type")]]来进行调整。关于visibility的详细内容,将在动态链接中讲解。
  • st_shndx 符号所在的段的index
  • st_value 符号值,不同的符号类型,其符号值的含义不同。
  • st_size 符号的大小,例如一个表示double类型数据的符号,其大小一般为8字节

st_info的高四位可以通过下面的宏来提取。

1
#define ELF64_ST_BIND(info)          ((info) >> 4)

其高四位的可能取值为:

  • STB_LOCAL 局部符号。局部符号包含其定义的目标文件外部不可见。多个文件可以存在同名但不相互指涉的局部符号。
  • STB_GLOBAL 全局符号 全局符号对所有链接过程中合并的目标文件都可见。一个文件对全局符号的定义,应满足另一个文件对同一符号的未定义引用。
  • STB_WEAK **弱全局符号**

其低四位的可以通过下面的宏来提取

1
#define ELF64_ST_TYPE(info)          ((info) & 0xf)

其低四位可能的取值为:

  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Symbol table '.symtab' contains 23 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS elf.cpp
2: 0000000000000000 0 SECTION LOCAL DEFAULT 2
3: 0000000000000000 0 SECTION LOCAL DEFAULT 4
4: 0000000000000000 0 SECTION LOCAL DEFAULT 5
5: 0000000000000000 0 SECTION LOCAL DEFAULT 6
6: 0000000000000008 4 OBJECT LOCAL DEFAULT 5 _ZZ4mainE15static_not_ini
7: 0000000000000004 4 OBJECT LOCAL DEFAULT 4 _ZZ4mainE11static_init
8: 0000000000000000 0 SECTION LOCAL DEFAULT 7
9: 0000000000000000 0 SECTION LOCAL DEFAULT 10
10: 0000000000000000 0 SECTION LOCAL DEFAULT 11
11: 0000000000000000 0 SECTION LOCAL DEFAULT 12
12: 0000000000000000 0 SECTION LOCAL DEFAULT 9
13: 0000000000000000 0 SECTION LOCAL DEFAULT 1
14: 0000000000000000 4 OBJECT GLOBAL DEFAULT 5 global_not_init
15: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 global_init
16: 0000000000000004 4 OBJECT GLOBAL DEFAULT 5 global_init_as_zero
17: 0000000000000000 23 FUNC GLOBAL DEFAULT 2 _Z15global_functionv
18: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
19: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
20: 0000000000000000 33 FUNC WEAK DEFAULT 7 _Z24global_function_templ
21: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
22: 0000000000000017 36 FUNC GLOBAL DEFAULT 2 main
  • putsprintf_GLOBAL_OFFSET_TABLE_所在的section(Ndx)都为UND(undefined),绑定信息为GLOBAL。这些符号在elf.o中被引用,但是不定义在其中。_GLOBAL_OFFSET_TABLE_的作用是对Position-Independent Code中全局函数或全局变量进行寻址。Global Offset Tables

  • global_not_init的Ndx为5,即.bsssection。而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
6
extern "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_GLOBALBIND的符号,而忽略STB_WEAKBIND的符号。
  • 类似地,如果存在一个有SHN_COMMON作为st_shndx的符号和多个STB_WEAK的符号,链接器会选择SHN_COMMON的那一个。
  • 如果一个符号在所有的目标文件中都是STB_WEAK,则选择占用空间最大的那个。
    而在上面的例子中,我们看到函数模板的实例化_Z24global_function_templ是一个弱符号

    1
    2
    Num:    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函数的处理也和模板相同。