链接器的链接一般分为两步:
空间与地址分配:收集目标文件中所有的段,将他们分类合并。收集目标文件中所有的符号,将他们统一放到一个符号表中。
符号解析与重定位:根据段的重定位表,和上一步中重新调整过的地址,对符号进行重定位。 关于空间与地址分配 和符号解析与重定位 的问题,可以参考程序员的自我修养 中关于静态链接4.1和4.2两节。 本文主要关注链接中以下几方面问题:
模板去重
-ffunction-sections
,-fdata-sections
和-gc-sections
清除dead code
inline函数与inline变量
模板去重 C++编译采用包含模型(inclusion model),大多数编译器在进行模板具现化时采用贪婪实例化(greedy instantiaion)。这将导致一个模板的相同具现化可能出现在多个翻译单元。如果将这些模板具现化产生的符号都认为是GLOBAL
类型的符号的话,会导致出现multiple definition 的链接错误。 现在主流编译器采用的策略是:将每个模板的具现化都单独放在一个segment中。当链接器在链接过程中遇到多个相同的因模板具现化而产生的段时,选择其中一个,丢弃其他的。
1 2 3 4 5 6 7 8 9 10 #include <cstdio> template <typename T>decltype (auto ) add (T a, T b) { const static char s[] = "STRING_LITERAL" ; puts (s); return a + b; } int main () { return add (1 , 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 39 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 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 8 COMDAT 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 int unused_function () { return 1 ; } 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 58 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._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 void print1 () ;void print2 () ;#include <cstdio> void print1 () { puts ("USED" ); } void print2 () { puts ("UNUSED" ); } #include "unused.hpp" 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 3 String 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 #include <cstdio> 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 39 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] .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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 inline int x = 1 ;#include <cstdio> #include "iv.hpp" void print_addr () { printf ("%p\n" , &x); } #include <cstdio> #include "iv.hpp" int main () { printf ("%p\n" , &x); }
编译上述文件,检查得到的可重定位文件的符号表,发现x
的BIND类型为UNIQUE:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Symbol 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 2 3 4 5 6 7 8 9 __attribute__((noinline)) inline auto ilf () { const int i = 1 ; return i; } int main () { ilf (); return 0 ; }
编译上述文件,检查得到的可重定位文件的符号表,发现函数ilf()
的符号类型为WEAK:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Num: 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
函数在每个翻译单元中拥有相同的地址 的特性。