链接器的链接一般分为两步:
- 空间与地址分配:收集目标文件中所有的段,将他们分类合并。收集目标文件中所有的符号,将他们统一放到一个符号表中。
- 符号解析与重定位:根据段的重定位表,和上一步中重新调整过的地址,对符号进行重定位。
关于空间与地址分配和符号解析与重定位的问题,可以参考程序员的自我修养中关于静态链接4.1和4.2两节。
本文主要关注链接中以下几方面问题: - 模板去重
-ffunction-sections
,-fdata-sections
和-gc-sections
清除dead code- inline函数与inline变量
模板去重
C++编译采用包含模型(inclusion model),大多数编译器在进行模板具现化时采用贪婪实例化(greedy instantiaion)。这将导致一个模板的相同具现化可能出现在多个翻译单元。如果将这些模板具现化产生的符号都认为是GLOBAL
类型的符号的话,会导致出现multiple definition 的链接错误。
现在主流编译器采用的策略是:将每个模板的具现化都单独放在一个segment中。当链接器在链接过程中遇到多个相同的因模板具现化而产生的段时,选择其中一个,丢弃其他的。
1 |
|
将上面这段程序编译之后,查看其可重定位目标文件,我们会发现该函数模板的实例化被放在一个单独的段.text._Z3addIiEDc
中。且其符号_Z3addIiEDcT_S0_
BIND为WEAK。并且该实例化所产生的局部static
变量也被单独地放置在了一个段中。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
39Section 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
0000000000000008 0000000000000004 15 13 4
[ 2] .group GROUP 0000000000000000 00000048
000000000000000c 0000000000000004 15 14 4
[ 3] .text PROGBITS 0000000000000000 00000054
000000000000001a 0000000000000000 AX 0 0 1
[ 4] .rela.text RELA 0000000000000000 00000348
0000000000000018 0000000000000018 I 15 3 8
[ 5] .data PROGBITS 0000000000000000 0000006e
0000000000000000 0000000000000000 WA 0 0 1
[ 6] .bss NOBITS 0000000000000000 0000006e
0000000000000000 0000000000000000 WA 0 0 1
[ 7] .rodata._ZZ3addIi PROGBITS 0000000000000000 00000070
000000000000000f 0000000000000000 AG 0 0 8
[ 8] .text._Z3addIiEDc PROGBITS 0000000000000000 0000007f
0000000000000028 0000000000000000 AXG 0 0 1
[ 9] .rela.text._Z3add RELA 0000000000000000 00000360
0000000000000030 0000000000000018 IG 15 8 8
[10] .comment PROGBITS 0000000000000000 000000a7
0000000000000025 0000000000000001 MS 0 0 1
[11] .note.GNU-stack PROGBITS 0000000000000000 000000cc
0000000000000000 0000000000000000 0 0 1
[12] .note.gnu.propert NOTE 0000000000000000 000000d0
0000000000000020 0000000000000000 A 0 0 8
[13] .eh_frame PROGBITS 0000000000000000 000000f0
0000000000000058 0000000000000000 A 0 0 8
[14] .rela.eh_frame RELA 0000000000000000 00000390
0000000000000030 0000000000000018 I 15 13 8
[15] .symtab SYMTAB 0000000000000000 00000148
00000000000001b0 0000000000000018 16 13 8
[16] .strtab STRTAB 0000000000000000 000002f8
000000000000004f 0000000000000000 0 0 1
[17] .shstrtab STRTAB 0000000000000000 000003c0
00000000000000ac 0000000000000000 0 0 1
在.text
段的上面出现了两个.group
段,并且类型为从未见过的GROUP。GROUP类型的段是用来记录哪些段是相互关联的。
比如在本例中,函数模板中的static
变量的位置在其专属的.rodata._ZZ3addIi
段中,在之后的链接过程中,随着段的合并,其地址必然需要进行重定位。因此,该函数模板的具现化代码所在的段.text._Z3addIiEDcT_S0_
也就需要一个与之匹配的重定位段.rela.text._Z3addIiEDcT_S0_
。这两个段是密不可分的。如果这两个段中的一个被丢弃,或者两个段中的一个被另一个目标文件中的同名段代替,就会导致不正确的链接。为了避免这种情况的发生,编译器在产生目标文件时,用GROUP将这些段绑定在一起。而链接器将每个GROUP类型的section中记录的段都看作一个整体,要么全部包括,要么全部丢弃,不可分割。
使用readelf -g .group tem.o
来查看GROUP section中的内容:1
2
3
4
5
6
7
8COMDAT group section [ 1] `.group' [_ZZ3addIiEDcT_S0_E1s] contains 1 sections:
[Index] Name
[ 7] .rodata._ZZ3addIiEDcT_S0_E1s
COMDAT group section [ 2] `.group' [_Z3addIiEDcT_S0_] contains 2 sections:
[Index] Name
[ 8] .text._Z3addIiEDcT_S0_
[ 9] .rela.text._Z3addIiEDcT_S0_
其中COMDAT是GROUP节的Flag,目前有且仅有一种,也就是COMDAT。COMDAT表示这个group可能和另一个目标文件中的COMDAT group重复。当重复发生时,只有其中一个group可以被留下,其他都会被丢弃。
group当中的section必须有SHF_GROUP
作为他们的sh_flags
,如果链接器决定丢弃一个group,那么group中的所有成员就也将被丢弃。
当同一个模板的具现化出现在多个目标文件时,连接器就使用GROUP并配合其符号BIND来进行去重。
关于Section Group的更详细解释,见Section Groups - Oracle
函数级别链接
如果想要将所有的函数和数据像模板函数一样,放入自己专属的段中,我们可以使用gcc的编译选项:
-ffunction-sections
-fdata-sections
如果目标文件或这静态库在编译时使用了这两个编译选项,链接器再使用--gc-sections
选项,就可以进行dead code elimination,去掉没有用到的代码,减少二进制膨胀。
例子1:1
2
3
4
5
6
7
8// unused.cpp
int unused_function(){
return 1;
}
// main.cpp
int main(){
return 0;
}
将main.cpp
和unused.cpp
使用-ffunction-sections
和-fdata-sections
编译得到可重定位文件后,查看其目标文件的段表,发现每个函数确实被放置在了其专属的段中。其中int unused_function()
对应的段是.text._Z15unused_
,int main()
对应的段是.text.main
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58Section 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
0000000000000000 0000000000000000 AX 0 0 1
[ 2] .data PROGBITS 0000000000000000 00000040
0000000000000000 0000000000000000 WA 0 0 1
[ 3] .bss NOBITS 0000000000000000 00000040
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .text._Z15unused_ PROGBITS 0000000000000000 00000040
000000000000000f 0000000000000000 AX 0 0 1
[ 5] .comment PROGBITS 0000000000000000 0000004f
0000000000000025 0000000000000001 MS 0 0 1
[ 6] .note.GNU-stack PROGBITS 0000000000000000 00000074
0000000000000000 0000000000000000 0 0 1
[ 7] .note.gnu.propert NOTE 0000000000000000 00000078
0000000000000020 0000000000000000 A 0 0 8
[ 8] .eh_frame PROGBITS 0000000000000000 00000098
0000000000000038 0000000000000000 A 0 0 8
[ 9] .rela.eh_frame RELA 0000000000000000 00000200
0000000000000018 0000000000000018 I 10 8 8
[10] .symtab SYMTAB 0000000000000000 000000d0
0000000000000108 0000000000000018 11 10 8
[11] .strtab STRTAB 0000000000000000 000001d8
0000000000000021 0000000000000000 0 0 1
[12] .shstrtab STRTAB 0000000000000000 00000218
0000000000000082 0000000000000000 0 0 1
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
0000000000000000 0000000000000000 AX 0 0 1
[ 2] .data PROGBITS 0000000000000000 00000040
0000000000000000 0000000000000000 WA 0 0 1
[ 3] .bss NOBITS 0000000000000000 00000040
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .text.main PROGBITS 0000000000000000 00000040
000000000000000f 0000000000000000 AX 0 0 1
[ 5] .comment PROGBITS 0000000000000000 0000004f
0000000000000025 0000000000000001 MS 0 0 1
[ 6] .note.GNU-stack PROGBITS 0000000000000000 00000074
0000000000000000 0000000000000000 0 0 1
[ 7] .note.gnu.propert NOTE 0000000000000000 00000078
0000000000000020 0000000000000000 A 0 0 8
[ 8] .eh_frame PROGBITS 0000000000000000 00000098
0000000000000038 0000000000000000 A 0 0 8
[ 9] .rela.eh_frame RELA 0000000000000000 000001e8
0000000000000018 0000000000000018 I 10 8 8
[10] .symtab SYMTAB 0000000000000000 000000d0
0000000000000108 0000000000000018 11 10 8
[11] .strtab STRTAB 0000000000000000 000001d8
000000000000000f 0000000000000000 0 0 1
[12] .shstrtab STRTAB 0000000000000000 00000200
0000000000000072 0000000000000000 0 0 1
之后使用链接器,并加入-gc-setions
选项来清除未用到的函数,使用-print-gc-sections
来具体打印出哪些段被舍弃。1
$ gcc -Wl,--gc-sections -Wl,--print-gc-sections -o unused_clean unused.o main.o
其中使用-Wl
的目的是将后面的参数只传给链接器。运行后可以看到一些运行库的数据段和unused.o
中的unused_function
被舍弃了。1
2
3/usr/bin/ld: removing unused section '.rodata.cst4' in file '/usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o'
/usr/bin/ld: removing unused section '.data' in file '/usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o'
/usr/bin/ld: removing unused section '.text._Z15unused_functionv' in file 'unused.o'
例子二:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// unused.hpp
void print1();
void print2();
// unused.cpp
void print1(){
puts("USED");
}
void print2(){
puts("UNUSED");
}
// main.cpp
int main(){
print1();
return 0;
}
使用下面的编译选项编译后:1
2$ gcc -c -ffunction-sections -fdata-sections unused.cpp main.cpp
$ gcc -Wl, -gc-sections -Wl, -print-gc-sections -o unused_literal unused.o main.o
查看生成的共项目标文件的.rodata
,发现字符串字面量"UNUSED"
依然存在,并没有被丢弃:1
2
3String dump of section '.rodata':
[ 0] USED
[ 5] UNUSED
这是因为-fdata-sections
和[[gnu::section("name")]]
(__attribute__(section("name"))
)一样,只能用在某个有名字的标识符上。而字符串字面量则统一被放入.rodata
段中。在本例中.rodata
段中的字符串"USED"
因为被函数print1()
使用,因此.rodata
没有被链接器丢弃,进而就使得"UNUSED"
这个字符串保留了下来。为了修复这个问题,我们将这个两个字符串字面量初始化为某一个具名字符串即可。1
2
3
4
5
6
7
8
9
void print1(){
const static char str[] = "USED";
puts(str);
}
void print2(){
const static char str[] = "UNUSED";
puts(str);
}
检查编译后产生的目标文件,发现这两个字符串的确被放在了自己专属的段中了: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
39Section 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
0000000000000000 0000000000000000 AX 0 0 1
[ 2] .data PROGBITS 0000000000000000 00000040
0000000000000000 0000000000000000 WA 0 0 1
[ 3] .bss NOBITS 0000000000000000 00000040
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .rodata._ZZ6print PROGBITS 0000000000000000 00000040
0000000000000005 0000000000000000 A 0 0 1
[ 5] .text._Z6print1v PROGBITS 0000000000000000 00000045
0000000000000017 0000000000000000 AX 0 0 1
[ 6] .rela.text._Z6pri RELA 0000000000000000 00000348
0000000000000030 0000000000000018 I 15 5 8
[ 7] .rodata._ZZ6print PROGBITS 0000000000000000 0000005c
0000000000000007 0000000000000000 A 0 0 1
[ 8] .text._Z6print2v PROGBITS 0000000000000000 00000063
0000000000000017 0000000000000000 AX 0 0 1
[ 9] .rela.text._Z6pri RELA 0000000000000000 00000378
0000000000000030 0000000000000018 I 15 8 8
[10] .comment PROGBITS 0000000000000000 0000007a
0000000000000025 0000000000000001 MS 0 0 1
[11] .note.GNU-stack PROGBITS 0000000000000000 0000009f
0000000000000000 0000000000000000 0 0 1
[12] .note.gnu.propert NOTE 0000000000000000 000000a0
0000000000000020 0000000000000000 A 0 0 8
[13] .eh_frame PROGBITS 0000000000000000 000000c0
0000000000000058 0000000000000000 A 0 0 8
[14] .rela.eh_frame RELA 0000000000000000 000003a8
0000000000000030 0000000000000018 I 15 13 8
[15] .symtab SYMTAB 0000000000000000 00000118
00000000000001c8 0000000000000018 16 15 8
[16] .strtab STRTAB 0000000000000000 000002e0
0000000000000067 0000000000000000 0 0 1
[17] .shstrtab STRTAB 0000000000000000 000003d8
00000000000000c5 0000000000000000 0 0 1
链接后,可以发现.rodata
这个section已经不存在了。
inline关键字
inline
关键字的本意是提示编译器采用函数的内联替换而非进行函数调用。inline
的该含义是非强制的,编译器可以对标注inline
的函数不进行内联替换,也可以对不标注inline
的函数进行内联替换。
inline
关键字的另一个含义是
- 将函数声明为内联函数
- 将具有静态存储期的变量(即所有声明于命名空间作用域的对象和声明时带有
static
或extern
关键字的对象)声明为内联变量
内联函数和内联变量有以下特点: - 它们的定义必须在访问它们的翻译单元中可见
- 带有外部链接的内联函数和内联变量(比如不生命为
static
的)拥有下列属性:- 他们可以在程序中拥有多于一次定义。但要求所有的定义都出现的不同的翻译单元且定义等同。(例如声明并定义在头文件中的
inline
函数可能被#include
进多个翻译单元,进而使得其定义出现在多个翻译单元中。) - 他们必须在每个翻译单元中都被声明为
inline
- 他们在每个翻译单元中都有相同的地址
- 他们可以在程序中拥有多于一次定义。但要求所有的定义都出现的不同的翻译单元且定义等同。(例如声明并定义在头文件中的
inline 变量
1 | // iv.hpp |
编译上述文件,检查得到的可重定位文件的符号表,发现x
的BIND类型为UNIQUE:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18Symbol table '.symtab' contains 16 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS iv.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: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 9
8: 0000000000000000 0 SECTION LOCAL DEFAULT 10
9: 0000000000000000 0 SECTION LOCAL DEFAULT 11
10: 0000000000000000 0 SECTION LOCAL DEFAULT 8
11: 0000000000000000 0 SECTION LOCAL DEFAULT 1
12: 0000000000000000 4 OBJECT UNIQUE DEFAULT 6 x
13: 0000000000000000 35 FUNC GLOBAL DEFAULT 2 _Z10print_addrv
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
man nm:
The symbol is a unique global symbol. This is a GNU extension to the standard set of ELF symbol bindings. he symbol is a unique global symbol. This is a GNU extension to the standard set of ELF symbol bindings. For such a symbol the dynamic linker will make sure that in the entire process there is just one symbol with this name and type in use.
即链接器会保证在链接过程中,保留一个且仅有一个(因为是GLOBAL符号,不能舍弃)该符号。GNU正是依靠这个扩展实现了内联变量的特性。
inline
关键字在变量上的引用使得将拥有全局变量的库打包成header only成为了可能。
inline 函数
1 | __attribute__((noinline)) inline auto ilf(){ |
编译上述文件,检查得到的可重定位文件的符号表,发现函数ilf()
的符号类型为WEAK:1
2
3
4
5
6
7
8
9
10
11
12
13
14Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS inline_function.cc
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: 0000000000000000 0 SECTION LOCAL DEFAULT 8
7: 0000000000000000 0 SECTION LOCAL DEFAULT 9
8: 0000000000000000 0 SECTION LOCAL DEFAULT 10
9: 0000000000000000 0 SECTION LOCAL DEFAULT 7
10: 0000000000000000 0 SECTION LOCAL DEFAULT 1
11: 0000000000000000 22 FUNC WEAK DEFAULT 6 _Z3ilfv
12: 0000000000000000 20 FUNC GLOBAL DEFAULT 2 main
根据WEAK符号的特点,链接器会在所有的翻译单元生成的_Z3ilfv
当中选择一个,来满足inline
函数在每个翻译单元中拥有相同的地址的特性。