从符号表深入理解C++

对于编译器而言,符号的全部相关数据都在Elf32_Sym或Elf64_Sym,以Elf32_Sym为例了解一些编译器的知识,然后再分析C++的语法

1
2
3
4
5
6
7
8
9
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility under glibc>=2.2 */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;

st_info这个结构是用于区分各种类型的,也是这一篇博文的重点,这个char结构8位

  • 高 4 位用于表示 st_bind

    静态链接行为
    STB_LOCAL 允许在不同的目标文件中,存在多个相同名字的符号,符号之间相互隔离(无关联)。
    STB_GLOBAL 只允许一个目标文件存在 GLOBAL 符号定义,其它目标文件允许相同名字的未定义的符号引用,即不允许多个定义。
    STB_WEAK 允许在不同的目标文件中存在多个相同名字的符号。具体来说,如果同时存在一个 GLOBAL 符号和其它同名 WEAK 符号的定义,那就挑选 GLOBAL 符号(ignores the weak ones);如果存在多个同名 WEAK 符号的定义,事实标准是挑选 linker 解析过程中接触到的第一个符号。

​ 我这里特意强调了静态链接行为,因为静态链接和动态链接虽然对LOCAL的逻辑是一致的,但是动态链接器在符号重定位的时候,对GLOBAL和WEAK是一视同仁的

  • 低4位用于表示st_type

    • STT_FUNC: The symbol is associated with a function or other executable code.
    • STT_OBJECT: The symbol is associated with a data object.
    • STT_SECTION: The symbol is a section.

    这就比较好理解:函数,变量或者段

全局变量和函数

实验代码包含了4个变量和5个函数(变量没有inline):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int globalValue = 1;
static int staticValue = 1;
extern int externValue;
__attribute__((weak)) int weakValue;

void globalFunc() {}
static void staticFunc() {}
inline void inlineFunc() {}
extern void externFunc();
__attribute__((weak)) void weakFunc() {}

void test() {
inlineFunc(); //inline类型函数,C++中调用才会产生符号
externValue = 1; //extern类型,必须使用才会出现UND的符号
externFunc(); //同上
}

C的编译结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
~ gcc -c test.c
~ readelf -s test.o|grep Value
Num: Value Size Type Bind Vis Ndx Name
5: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 staticValue
11: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 globalValue
12: 0000000000000000 4 OBJECT WEAK DEFAULT 4 weakValue
18: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND externValue
~ readelf -s test.o|grep Func
Num: Value Size Type Bind Vis Ndx Name
6: 000000000000000b 11 FUNC LOCAL DEFAULT 1 staticFunc
13: 0000000000000000 11 FUNC GLOBAL DEFAULT 1 globalFunc
14: 0000000000000016 11 FUNC WEAK DEFAULT 1 weakFunc
17: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND inlineFunc
19: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND externFunc

在C语法中:

  • static类型的函数和变量会变成LOCAL(多个定义不会有问题),啥都不写的函数和变量是GLOBAL(多个定义会冲突)
  • inline类型的函数对C来说和extern含义是一样的,会产生UND标记
  • 正常情况下不会产生weak符号,除非使用__attribute__((weak))

C++的编译结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
~ g++ -c test.c
~ readelf -s test.o|grep Value
Num: Value Size Type Bind Vis Ndx Name
5: 0000000000000004 4 OBJECT LOCAL DEFAULT 4 _ZL11staticValue
13: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 globalValue
14: 0000000000000000 4 OBJECT WEAK DEFAULT 5 weakValue
19: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND externValue
~ readelf -s test.o|grep Func
Num: Value Size Type Bind Vis Ndx Name
6: 000000000000000b 11 FUNC LOCAL DEFAULT 2 _ZL10staticFuncv
15: 0000000000000000 11 FUNC GLOBAL DEFAULT 2 _Z10globalFuncv
16: 0000000000000000 11 FUNC WEAK DEFAULT 6 _Z10inlineFuncv
17: 0000000000000016 11 FUNC WEAK DEFAULT 2 _Z8weakFuncv
21: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _Z10externFuncv

在C++语法中:

  • 首先函数符号的样式全变了,可以使用c++filt还原,但是变量除了static还保持原样(没什么卵用的冷知识+1)

  • inline函数的符号不再和extern一样,而是和使用__attribute__((weak))声明是一样的

    __attribute__((weak))声明的区别是使用场景上,必须调用才会出现符号

实验代码包含了2种变量和4种函数,非static变量没有inline属性所以少一个用例,函数多一个定义在类外的主动声明inline,所以多一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct foo {
int Value;
static int staticValue;

void inlineFunc() {}
void globalFunc();
void anotherInlineFunc(); //为了说明类外定义也可以加上inline
static void inlineStaticFunc() {} //为了说明默认static也是inline的
static void globalStaticFunc();
};

int foo::staticValue = 1;

void foo::globalFunc() {}
inline void foo::anotherInlineFunc() {}
void foo::globalStaticFunc() {}

void test() {
foo f;
f.inlineFunc(); //inline类型函数,C++中调用才会产生符号
f.anotherInlineFunc(); //同上
f.inlineStaticFunc(); //static成员函数也是默认inline
}

编译结果如下:

1
2
3
4
5
6
7
8
9
10
11
~ g++ -c testClass.cpp
~ readelf -sW testClass.o|grep Value
Num: Value Size Type Bind Vis Ndx Name
14: 0000000000000000 4 OBJECT GLOBAL DEFAULT 5 _ZN3foo11staticValueE
~ readelf -sW testClass.o|grep Func
Num: Value Size Type Bind Vis Ndx Name
15: 0000000000000000 15 FUNC WEAK DEFAULT 8 _ZN3foo10inlineFuncEv
16: 0000000000000000 11 FUNC WEAK DEFAULT 9 _ZN3foo16inlineStaticFuncEv
18: 0000000000000000 15 FUNC GLOBAL DEFAULT 4 _ZN3foo10globalFuncEv
19: 0000000000000000 15 FUNC WEAK DEFAULT 10 _ZN3foo17anotherInlineFuncEv
20: 0000000000000010 11 FUNC GLOBAL DEFAULT 4 _ZN3foo16globalStaticFuncEv
  • 变量上只有static类型的,才会产生GLOBAL符号,否则作为局部变量会在堆栈上创建
  • 类内声明和定义的函数自带inline(包括static成员函数),因此也需要调用才会出现符号,性质和普通全局inline函数一致

函数内静态变量

  • testFuncStaticValue.cpp:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    void globalFunc() {
    static int value = 1;
    value = 2; //静态变量必须使用一次才会有符号
    }
    inline void inlineFunc() {
    static int value = 1;
    value = 2;
    }
    static void staticFunc() {
    static int value = 1;
    value = 2;
    }

    void test() {
    inlineFunc(); //inline属性必须调用才会有符号
    }
  • main.cpp:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //void globalFunc()		不能定义,会发生重定义报错
    inline void inlineFunc() {
    static int value = 1;
    value = 2; //静态变量必须使用一次才会有符号
    }
    static void staticFunc() {
    static int value = 1;
    value = 2;
    }

    int main() {
    inlineFunc(); //inline属性必须调用才会有符号
    }

编译结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
~ g++ -c testFuncStaticValue.cpp
~ g++ testFuncStaticValue.cpp main.cpp -o main
~ readelf -sW testFuncStaticValue.o|grep value
Num: Value Size Type Bind Vis Ndx Name
5: 0000000000000000 4 OBJECT LOCAL DEFAULT 5 _ZZ10globalFuncvE5value
8: 0000000000000004 4 OBJECT LOCAL DEFAULT 5 _ZZL10staticFuncvE5value
17: 0000000000000000 4 OBJECT UNIQUE DEFAULT 7 _ZZ10inlineFuncvE5value
~ readelf -sW main|grep value
Num: Value Size Type Bind Vis Ndx Name
35: 0000000000004010 4 OBJECT LOCAL DEFAULT 23 _ZZ10globalFuncvE5value
36: 0000000000004014 4 OBJECT LOCAL DEFAULT 23 _ZZL10staticFuncvE5value
39: 000000000000401c 4 OBJECT LOCAL DEFAULT 23 _ZZL10staticFuncvE5value
56: 0000000000004018 4 OBJECT UNIQUE DEFAULT 23 _ZZ10inlineFuncvE5value

函数内静态变量的属性是和函数本身相关的:

  • 对于默认情况(没有inline或static)的函数而言,会生成LOCAL属性的变量,由于函数本身不能重定义,所以虽然是LOCAL,也只会有一个实例

  • 对于static的函数而言,会生成LOCAL属性的变量,编译后会有多个实例

  • 对于inline的函数而言,会生成UNIQUE属性的变量,全称STB_GNU_UNIQUE

    这个变量类型作为GNU独属,是之前没有讨论过的,它的争议很大,作为全局唯一的标识,在下面一节介绍

这里还带来一个关于静态库和动态库的问题:

LOCAL属性也意味着动态或静态链接库中的默认情况函数静态变量是不能对外导出的,只有通过设置inline属性,将会设定为跨库唯一的符号

  • 要么inline,对外导出并全局唯一
  • 要么static或者空着,不对外导出并全局多个副本

不存在既不导出,又全局唯一的情况,一旦选择了inline,使用 RTLD_LOCAL 方式装载也只允许一个实例!

STB_GNU_UNIQUE

搜索这个关键词可以看到很多吐槽,例如

  • Patching STB_GNU_UNIQUE of Buggy Binaries

    STB_GNU_UNIQUE 就是 ELF 中一个不太好的设计, 带来了不少语义冲突. 拥有 STB_GNU_UNIQUE binding 的符号, 即使在被用 RTLD_LOCAL 方式装载的时候, 也会拥有 global linkage. 另外它还会导致 dlclose 无效

    这个 binding 最初的引入似乎是由于一些全局符号的内在状态不能重复多次, 因此把这些符号标记为 unique, 即使从多个 plugins 里装载了多次, 符号也只有一个定义. 但是另一方面, 程序也会有一些全局符号的状态必须是 local 的. 到底哪种行为是用户需要的, 编译器是不知道的. 结果是, gcc "聪明" 的自动把 template function & inline function 里的 static variable 标记为了 unique

  • static link stdc++ without STB_GNU_UNIQUE cause memory leak when dlclose

  • https://news.ycombinator.com/item?id=21555752

    The GNU executable and linking model has a ton of warts and I wish we could start from scratch. I recently found out about another ugly wart: STB_GNU_UNIQUE. Over a decade ago, someone noticed programs crashing due to symbol collisions causing use-after-free bugs in std::string, so instead of fixing the symbol collision, this person added a flag (STB_GNU_UNIQUE) that would force the colliding symbols ("vague linkage" ones) to be merged even among different libraries loaded with RTLD_LOCAL. Later, someone noticed that due to this merging, if you unloaded a library that exported a STB_GNU_UNIQUE symbol, other, unrelated libraries would crash. The awful solution to that problem was just to make these STB_GNU_UNIQUE libraries immune to dlclose.

    GNU 可执行文件和链接模型有很多缺陷,我希望我们可以从头开始。我最近发现了另一个丑陋的疣:STB_GNU_UNIQUE。十多年前,有人注意到由于符号冲突导致程序崩溃,导致 std::string 中的释放后使用错误,因此该人没有修复符号冲突,而是添加了一个标志(STB_GNU_UNIQUE)来强制冲突符号(“模糊链接”)甚至可以在加载 RTLD_LOCAL 的不同库之间进行合并。后来,有人注意到,由于这种合并,如果您卸载导出 STB_GNU_UNIQUE 符号的库,其他不相关的库将会崩溃。这个问题的糟糕解决方案就是让这些 STB_GNU_UNIQUE 库不受 dlclose 的影响。

总而言之,为了让inline属性的函数静态变量在多个动态库中依然只有一个实例,新增了STB_GNU_UNIQUE,这个特性会导致动态库无法被卸载

解决办法是-fno-gnu-unique,或者自己对库进行符号替换

-fno-gnu-unique On systems with recent GNU assembler and C library, the C++ compiler uses the "STB_GNU_UNIQUE" binding to make sure that definitions of template static data members and static local variables in inline functions are unique even in the presence of "RTLD_LOCAL"; this is necessary to avoid problems with a library used by two different "RTLD_LOCAL" plugins depending on a definition in one of them and therefore disagreeing with the other one about the binding of the symbol. But this causes "dlclose" to be ignored for affected DSOs; if your program relies on reinitialization of a DSO via "dlclose" and "dlopen", you can use -fno-gnu-unique.

模板

同一个模板在每个用到的编译单元都会进行实例化,最后重复的实例会在链接阶段去重

  • 标准中设定模板函数默认为inline属性,虽然可以设置为static,但是将其设置为static没有任何意义。

    Why use static function template?

    For template functions, I'd say that using static, inline or no keyword produce the same result; in fact, in all cases the function will be inlined and no multiple definition error will be raised. 对于模板函数,我想说使用staticinline或不使用关键字会产生相同的结果;事实上,在所有情况下,函数都会被内联,并且不会引发多重定义错误。

  • 模板的每个实例都会有自己的static静态变量

    Static variable inside template function

    The ODR (One Definition Rule) says at 3.2/5 in the Standard, where D stands for the non-static function template (cursive font by me)

    你可以依赖这个。ODR(One Definition Rule,一次定义规则)在标准的3.2/5处提到,其中D代表非静态的函数模板

    If D is a template, and is defined in more than one translation unit, then the last four requirements from the list above shall apply to names from the template’s enclosing scope used in the template definition (14.6.3), and also to dependent names at the point of instantiation (14.6.2). If the definitions of D satisfy all these requirements, then the program shall behave as if there were a single definition of D. If the definitions of D do not satisfy these requirements, then the behavior is undefined.

    如果D是一个模板,并且在多于一个的翻译单元中定义,那么上述列表中的最后四个要求应应用于在模板定义中用到的来自模板封装作用域的名字(14.6.3),以及在实例化点的依赖名字(14.6.2)。如果D的定义满足所有这些要求,那么程序将表现得好像D只有一个定义。如果D的定义不满足这些要求,那么行为是未定义的。

    Of the last four requirements, the two most important are roughly

    在最后四个要求中,最重要的两个大概是:

    • each definition of D shall consist of the same sequence of tokens

      模板的每个定义应该由相同的代码组成。也就是说,不同代码文件中的模板定义应该完全相同。

    • names in each definition shall refer to the same things ("entities")

      定义中使用的名字,例如变量名、类型名等,应该指向相同的事情。例如,如果你在一个文件中的模板定义中使用了名字foo来指向一个变量,在另一个文件中的定义也应该使用foo来指向同一个变量,而不是一个不相关的变量或函数。

编译器实现(静态链接)

具体来说:C++编译采用包含模型(inclusion model),大多数编译器在进行模板具现化时采用贪婪实例化(greedy instantiaion)。这将导致一个模板的相同具现化可能出现在多个翻译单元。如果将这些模板具现化产生的符号都认为是GLOBAL类型的符号的话,会导致出现multiple definition 的链接错误。

现在主流编译器采用的策略是:将每个模板的具现化都单独放在一个segment中。当链接器在链接过程中遇到多个相同的因模板具现化而产生的段时,选择其中一个,丢弃其他的。

简单编写代码来验证编译器的具体实现,在两个cpp文件里面引用同一个模板,编译成目标文件后,看下符号表

本文没有讨论动态链接,因为模板第三方库大多是headonly的

代码如下:

  • common.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #pragma once

    #include <cstdio>

    template <typename T>
    void foo() {
    static char s[] = "";
    puts(s);
    }

    template <typename>
    struct test {
    void hello() {
    }
    };
  • main.cpp

    1
    2
    3
    4
    5
    6
    7
    #include "common.h"

    int main() {
    foo<int>();
    test<int> t;
    t.hello();
    }
  • other.cpp

    1
    2
    3
    4
    5
    6
    7
    #include "common.h"

    void other() {
    foo<int>();
    test<int> t;
    t.hello();
    }

编译目标文件

1
gcc -c main.cpp other.cpp

分别查看符号表,可以发现模板函数是一个弱符号,相当于自带了inline,根据函数内静态变量,带上inline后,它的静态变量是UNIQUE属性,并且函数和静态变量值都为0:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
~ readelf -s other.o

Symbol table '.symtab' contains 13 entries:
Num: Value Size Type Bind Vis Ndx Name
16: 0000000000000000 23 FUNC WEAK DEFAULT 9 _Z3fooIiEvv
17: 0000000000000000 15 FUNC WEAK DEFAULT 11 _ZN4testIiE5helloEv
20: 0000000000000000 1 OBJECT UNIQUE DEFAULT 8 _ZZ3fooIiEvvE1s

~ readelf -s main.o

Symbol table '.symtab' contains 13 entries:
Num: Value Size Type Bind Vis Ndx Name
16: 0000000000000000 23 FUNC WEAK DEFAULT 9 _Z3fooIiEvv
17: 0000000000000000 15 FUNC WEAK DEFAULT 11 _ZN4testIiE5helloEv
20: 0000000000000000 1 OBJECT UNIQUE DEFAULT 8 _ZZ3fooIiEvvE1s

~ c++filt _Z3fooIiEvv
void foo<int>()
~ c++filt _ZN4testIiE5helloEv
test<int>::hello()
~ c++filt _ZZ3fooIiEvvE1s
foo<int>()::s

分别查看段表,可以发现这些符号被独立出来了section,并且标记了Flags

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
~ readelf -S other.o
There are 20 section headers, starting at offset 0x588:

Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 8] .bss._ZZ3fooIiEvv NOBITS 0000000000000000 0000009f
0000000000000001 0000000000000000 WAG 0 0 1
[ 9] .text._Z3fooIiEvv PROGBITS 0000000000000000 0000009f
0000000000000017 0000000000000000 AXG 0 0 1
[10] .rela.text._Z3foo RELA 0000000000000000 00000458
0000000000000030 0000000000000018 IG 17 9 8
[11] .text._ZN4testIiE PROGBITS 0000000000000000 000000b6
000000000000000f 0000000000000000 AXG 0 0 2

~ readelf -S main.o
There are 20 section headers, starting at offset 0x588:

Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 8] .bss._ZZ3fooIiEvv NOBITS 0000000000000000 0000009f
0000000000000001 0000000000000000 WAG 0 0 1
[ 9] .text._Z3fooIiEvv PROGBITS 0000000000000000 0000009f
0000000000000017 0000000000000000 AXG 0 0 1
[10] .rela.text._Z3foo RELA 0000000000000000 00000458
0000000000000030 0000000000000018 IG 17 9 8
[11] .text._ZN4testIiE PROGBITS 0000000000000000 000000b6
000000000000000f 0000000000000000 AXG 0 0 2

查阅文档的含义

  • A (Alloc): 表示段将被加载到内存中。
  • X (Execute): 可执行代码段。
  • I (INFO LINK): 表示这个重定位段与一个符号表关联(可能是包含符号表索引的段)。
  • G (Group): 属于一个特定的段分组。

和其他段相比,他多了一个组属性,readelf有专门的指令可以查看组

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
~ readelf -g other.o

COMDAT group section [ 1] `.group' [_ZZ3fooIiEvvE1s] contains 1 sections:
[Index] Name
[ 8] .bss._ZZ3fooIiEvvE1s

COMDAT group section [ 2] `.group' [_Z3fooIiEvv] contains 2 sections:
[Index] Name
[ 9] .text._Z3fooIiEvv
[ 10] .rela.text._Z3fooIiEvv

COMDAT group section [ 3] `.group' [_ZN4testIiE3fooEv] contains 1 sections:
[Index] Name
[ 11] .text._ZN4testIiE5helloEv


~ readelf -g main.o

COMDAT group section [ 1] `.group' [_ZZ3fooIiEvvE1s] contains 1 sections:
[Index] Name
[ 8] .bss._ZZ3fooIiEvvE1s

COMDAT group section [ 2] `.group' [_Z3fooIiEvv] contains 2 sections:
[Index] Name
[ 9] .text._Z3fooIiEvv
[ 10] .rela.text._Z3fooIiEvv

COMDAT group section [ 3] `.group' [_ZN4testIiE3fooEv] contains 1 sections:
[Index] Name
[ 11] .text._ZN4testIiE5helloEv

其中COMDAT是GROUP段的Flag,目前有且仅有一种,也就是COMDAT。COMDAT表示这个group可能和另一个目标文件中的COMDAT group重复。当重复发生时,只有其中一个group可以被留下,其他都会被丢弃。如果链接器决定丢弃一个group,那么group中的所有成员就也将被丢弃。

.text._Z3fooIiEvv.rela.text._Z3fooIiEvv在同一个组,这是因为根据汇编代码(main.o也一样,省略了),因为一方需要另外一个的重定位静态变量信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
~ objdump -S other.o
Disassembly of section .text._Z3fooIiEvv:

0000000000000000 <_Z3fooIiEvv>:
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 <_Z3fooIiEvv+0xf>
f: e8 00 00 00 00 callq 14 <_Z3fooIiEvv+0x14>
14: 90 nop
15: 5d pop %rbp
16: c3 retq

~ objdump -r other.o

other.o: file format elf64-x86-64

RELOCATION RECORDS FOR [.text._Z3fooIiEvv]:
OFFSET TYPE VALUE
000000000000000b R_X86_64_PC32 _ZZ3fooIiEvvE1s-0x0000000000000004
0000000000000010 R_X86_64_PLT32 puts-0x0000000000000004

.text._Z3fooIiEvv这个段不在了,那么重定位段也没必要存在了

链接可执行文件

1
~ gcc other.o main.o -o main

观察符号表

1
2
3
4
5
6
7
8
9
10
~ readelf -s main|grep foo
Symbol table '.symtab' contains 69 entries:
Num: Value Size Type Bind Vis Ndx Name
48: 0000000000004011 1 OBJECT UNIQUE DEFAULT 26 _ZZ3fooIiEvvE1s
67: 0000000000001159 23 FUNC WEAK DEFAULT 16 _Z3fooIiEvv

~ readelf -s main|grep hello
Symbol table '.symtab' contains 69 entries:
Num: Value Size Type Bind Vis Ndx Name
55: 00000000000011c8 15 FUNC WEAK DEFAULT 16 _ZN4testIiE5helloEv

可以发现此时每个符号只出现了一次,并且值都有了,而group段此时已经不见了

1
2
3
~ readelf -g main

There are no section groups in this file.

特化模板编译

特化模板函数并不是一个模板,而是一个实际的函数,需要手动添加inline才可以防止重定义

根据可知,类内定义的成员函数自带inline,不会重定义,类外的则需要

实验代码如下:

  • common.h

    1
    2
    3
    4
    5
    6
    7
    #pragma once

    template <typename>
    void foo() {}

    template <typename>
    struct test {};
  • main.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include "common.h"

    template <>
    void foo<int>() {}

    template <>
    struct test<int> {
    void hello() {}
    };

    int main() {
    test<int> t;
    t.hello(); //类内定义的成员函数inline,因此需要使用才会有符号
    //test<int>(); //特化普通函数不用
    }
  • other.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include "common.h"

    template <>
    void foo<int>() {}

    template <>
    struct test<int> {
    void hello() {}
    };

    void test_foo() {
    test<int> t;
    t.hello(); //类内定义的成员函数inline,因此需要使用才会有符号
    //test<int>(); //特化普通函数不用
    }

编译和链接报错:

1
2
3
4
5
~ gcc -c main.cpp other.cpp
~ gcc main.o other.o -o main
/usr/bin/ld: /tmp/ccwXjzCs.o: in function `void test<int>()':
other.cpp:(.text+0x0): multiple definition of `void test<int>()'; /tmp/ccpkd1rr.o:main.cpp:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status

可以发现,对模板类特化的inline属性的foo函数是没有任何报错信息的

相反,对于特化普通函数foo<int>来说,出现了报错,然后看看符号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
~ readelf -s other.o

Symbol table '.symtab' contains 16 entries:
Num: Value Size Type Bind Vis Ndx Name

11: 0000000000000000 11 FUNC GLOBAL DEFAULT 2 _Z3fooIiEvv
12: 0000000000000000 15 FUNC WEAK DEFAULT 6 _ZN4testIiE5helloEv

~ readelf -s main.o

Symbol table '.symtab' contains 16 entries:
Num: Value Size Type Bind Vis Ndx Name

11: 0000000000000000 11 FUNC GLOBAL DEFAULT 2 _Z3fooIiEvv
12: 0000000000000000 15 FUNC WEAK DEFAULT 6 _ZN4testIiE5helloEv

可以发现对模板函数foo<int>声明为了GLOBAL,因此报错;而对模板类函数声明成了WEAK,因此没有报错

并且结合在上一节编译目标文件的内容,可以发现test<int>::hello()特化模板类它依然是普通模板去重逻辑,会产生组section

1
2
3
4
5
6
7
8
9
10
11
~ readelf -g other.o

COMDAT group section [ 1] `.group' [_ZN4testIiE5helloEv] contains 1 sections:
[Index] Name
[ 6] .text._ZN4testIiE5helloEv

~ readelf -g main.o

COMDAT group section [ 1] `.group' [_ZN4testIiE5helloEv] contains 1 sections:
[Index] Name
[ 6] .text._ZN4testIiE5helloEv

加速编译

根据上面的测试,可以发现编译缓慢的最大原因是对模板类,每个文件都需要实例化一次,那么有没有办法只在一个目标文件中实例化,其他目标文件等链接的时候直接使用呢

声明和定义分离

很容易想到将声明和定义分离来解决这个问题

  • common.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #pragma once

    template <typename>
    void foo();

    template <typename>
    struct test {
    void hello();
    };
  • main.cpp

    1
    2
    3
    4
    5
    6
    7
    #include "common.h"

    int main() {
    foo<int>();
    test<int> t;
    t.hello();
    }
  • other.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include "common.h"

    template <typename>
    void foo() {
    }

    template <typename T>
    void test<T>::hello() {
    }

    void other() {
    foo<int>();
    test<int> t;
    t.hello();
    }

编译和链接:

1
2
~ gcc -c main.cpp other.cpp
~ gcc main.o other.o -o main

查看符号表:

1
2
3
4
5
6
7
8
9
10
11
12
13
~ readelf -s other.o

Symbol table '.symtab' contains 18 entries:
Num: Value Size Type Bind Vis Ndx Name
14: 0000000000000000 11 FUNC WEAK DEFAULT 7 _Z3fooIiEvv
15: 0000000000000000 15 FUNC WEAK DEFAULT 8 _ZN4testIiE5helloEv

~ readelf -s main.o

Symbol table '.symtab' contains 14 entries:
Num: Value Size Type Bind Vis Ndx Name
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _Z3fooIiEvv
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _ZN4testIiE5helloEv

可以发现main.o中这两个符号在Ndx中都标记了UND,而other.o中是存在的

再看下分组信息确定模板有没有实例化,main.o确实不存在分组信息了:

1
2
3
4
5
6
7
8
9
10
11
12
13
~ readelf -g other.o

COMDAT group section [ 1] `.group' [_Z3fooIiEvv] contains 1 sections:
[Index] Name
[ 7] .text._Z3fooIiEvv

COMDAT group section [ 2] `.group' [_ZN4testIiE5helloEv] contains 1 sections:
[Index] Name
[ 8] .text._ZN4testIiE5helloEv

~ readelf -g main.o

There are no section groups in this file.

很明显,加速编译的目标达成了,分析可执行文件符号都找到了

1
2
3
4
~ readelf -s main
Num: Value Size Type Bind Vis Ndx Name
53: 00000000000011de 15 FUNC WEAK DEFAULT 16 _ZN4testIiE5helloEv
65: 00000000000011d3 11 FUNC WEAK DEFAULT 16 _Z3fooIiEvv

extern模板

extern模板是C++11引入的功能,正常的模板代码的目标文件,进行extern声明以后,就不再会进行模板的实例化了

  • common.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #pragma once

    template <typename>
    void foo() {
    }

    template <typename>
    struct test {
    void hello() {
    }
    };
  • main.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include "common.h"

    extern template void foo<int>();
    extern template struct test<int>;

    int main() {
    foo<int>();
    test<int> t;
    t.hello();
    }
  • other.cpp

    1
    2
    3
    4
    5
    6
    7
    #include "common.h"

    void other() {
    foo<int>();
    test<int> t;
    t.hello();
    }

编译和链接:

1
2
~ gcc -c main.cpp other.cpp
~ gcc main.o other.o -o main

查看符号表:

1
2
3
4
5
6
7
8
9
10
11
12
13
~ readelf -s other.o

Symbol table '.symtab' contains 18 entries:
Num: Value Size Type Bind Vis Ndx Name
14: 0000000000000000 11 FUNC WEAK DEFAULT 7 _Z3fooIiEvv
15: 0000000000000000 15 FUNC WEAK DEFAULT 8 _ZN4testIiE5helloEv

~ readelf -s main.o

Symbol table '.symtab' contains 14 entries:
Num: Value Size Type Bind Vis Ndx Name
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _Z3fooIiEvv
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _ZN4testIiE5helloEv

和声明定义分离中一样,可以发现main.o中这两个符号在Ndx中也都标记了UND,而other.o中是存在的

再看下分组信息确定模板有没有实例化,main.o确实不存在分组信息了:

1
2
3
4
5
6
7
8
9
10
11
12
13
~ readelf -g other.o

COMDAT group section [ 1] `.group' [_Z3fooIiEvv] contains 1 sections:
[Index] Name
[ 7] .text._Z3fooIiEvv

COMDAT group section [ 2] `.group' [_ZN4testIiE5helloEv] contains 1 sections:
[Index] Name
[ 8] .text._ZN4testIiE5helloEv

~ readelf -g main.o

There are no section groups in this file.

和声明定义分离中一样,加速编译的目标也达成了,分析可执行文件符号都找到了

1
2
3
4
~ readelf -s main
Num: Value Size Type Bind Vis Ndx Name
53: 00000000000011de 15 FUNC WEAK DEFAULT 16 _ZN4testIiE5helloEv
65: 00000000000011d3 11 FUNC WEAK DEFAULT 16 _Z3fooIiEvv

总结

首先只有全局可见的东西才会在符号表中存在,否则只是局部变量

符号表有如下类型:

静态链接行为
STB_LOCAL 允许在不同的目标文件中,存在多个相同名字的符号,符号之间相互隔离(无关联)。
STB_GLOBAL 只允许一个目标文件存在 GLOBAL 符号定义,其它目标文件允许相同名字的未定义的符号引用,即不允许多个定义。
STB_WEAK 允许在不同的目标文件中存在多个相同名字的符号。具体来说,如果同时存在一个 GLOBAL 符号和其它同名 WEAK 符号的定义,那就挑选 GLOBAL 符号(ignores the weak ones);如果存在多个同名 WEAK 符号的定义,事实标准是挑选 linker 解析过程中接触到的第一个符号。

静态链接和动态链接虽然对LOCAL的逻辑是一致的,但是动态链接器在符号重定位的时候,对GLOBAL和WEAK是一视同仁的

重定位细节参考hook的妙用

  • 全局变量和函数:

    • static作用域是LOCAL类型,在静态和动态编译中只对当前编译单元可见
    • inline
      • 对C来说和extern是一样的含义
      • 对C++来说是WEAK类型,并且需要调用才会产生符号
    • 默认情况(没有static或inline)则是GLOBAL属性
  • 类:

    • 类内声明和定义的函数自带inline(包括static成员函数)
  • 函数内静态变量

    静态变量的符号和函数本身的前缀有关,和普通全局变量的区别是对外导出和全局唯一是绑定的

    • static,会生成LOCAL属性的变量,编译后会有多个实例

    • inline,会生成UNIQUE属性的变量,全称STB_GNU_UNIQUE

      UNIQUE属性全局唯一,即使跨动态库,因此会造成dlclose失效

    • 默认情况(没有static或inline),会生成LOCAL属性的变量,由于函数本身不能重定义,所以虽然是LOCAL,也只会有一个实例

  • 模板

    本文没有讨论动态链接,因为模板第三方库大多是headonly的

    • 同一个模板在每个用到的编译单元都会进行实例化生成group信息帮助重定位,最后重复的实例会在链接阶段去重,所以inline和static或者默认都一样

    • 每个模板类型实例化以后都会拥有自己的static变量

    • 特化函数模板必须标记inline才不会造成重定义(对全局函数而言,标static也行,就是浪费了)

    • 加速编译可以使用声明和定义分离,也可以使用extern模板

      • extern模板

        优点:对代码侵入性小,加一行代码就行

        缺点:新增编译单元以后容易忘记加

      • 声明和定义分离

        优点:不用额外加一行代码,不存在忘记的问题,因为编译单元include的只是一个声名

        缺点:

        对代码侵入性大,要把所有的函数都拆到单独的cpp文件里面去

        需要在定义的cpp文件里面实例化好类型,因此只适用于自己的代码,如果是作为模板库对外暴露,那么无法预先知道会被实例化成什么类型

      综合考虑extern模板肯定是更方便的,为了解决extern模板的缺陷,可以考虑把extern声明和模板本身放在一起,然后通过宏定义在某个文件中屏蔽掉extern声明,从而集中在这个文件中实例化

参考资料

ELF Format Cheatsheet