符号(symbol)是在 ELF格式中会遇到的概念,也就是在写汇编代码时候会遇到的,而在更高级语言(C或者C++)中不会直接遇到这个概念,我们把讨论的范围限制在 Linux上的ELF格式。
符号的绑定(Symbol Binding)符号和符号之间是不一样的,首先我们明确,链接器的任务是把很多个.o文件(object file)组合到一起,因为一个符号可能在某一个 object file中定义了,而在另一个object file 中使用了,链接器的任务就是把这些 reference都分析清楚。
常见的符号有三种类型:
看下面一个例子:
- // 我们在 symbol.cpp 源文件中定义一个函数 func
- symbol.cpp
-
- int func() {
- return 1;
- }

经过 gcc -c -S main.cpp -o maini.o 命令编译后生成 main.o文件 // 然后 cat main.o 得到上面汇编代码
可以看到 函数名称 func就是一个符号,如果想要调用这个函数,就可以利用这个函数的符号
有人说,汇编后的代码没有看到 func 符号,其实这是C++s实现了函数的重载

下面在介绍下,在链接中可能会出现问题。看下面的代码
- // a.h
- #pragma once
- int func(){
- return 0;
- }
-
-
- // A.cpp
-
- #include"a.h"
- void printl_Fun() {
- func();
- }
-
- // main.cpp
-
-
- #include"a.h"
-
- int main() {
- func();
- return 0;
- }
很显然:如果你编译两个.cpp文件,那么就是在编译 A.cpp和main.cpp的时候分别生成 Global Symbol (func) ,这样在链接的时候就会报错: func 重定义



显然报错了:multipe definition of 'func()' 重复定义函数 func()
- // A.h
- #pragma once
- int func();
-
- // A.cpp
-
- #include"a.h"
- int func()
- {
- return 0;
- }
-
-
- // main.cpp
-
- #include
- #include"a.h"
-
- int main() {
- int ret = func();
- std::cout << ret << std::endl;
- return 0;
- }
看下预编译和编译成汇编的结果:在main.o中是看不到 全局符号(global symbol) func的

在看下 A.o的汇编结果: 很显然是存储 全局符号(Global Symbol)func

所以这就很清晰了,符号 func 在全局符号表中只有一个,链接的时候,自然就不会后什么问题。
这样做最后的二进制文件会变大一些. 具体在 C++ 中有两种做法, 拿我们的 func 函数示例. 可以加上 static. 这个在C++层面称之为: Internal Linkage
- // a.h
-
-
- #pragma once
-
- //static int func() {
- // // static修饰的 函数,会生成local symbol
- // return 1;
- //};
-
- // 方式二 : 匿名的 namespace里面都是 Local Symbal符号
- namespace {
- int func() {
- return 1;
- }
- }
-
- A.cpp
-
- #include
- #include"a.h"
- void printf_fun()
- {
- std::cout << func() << std::endl;
- }
-
- // main.cpp
-
- #include
- #include"a.h"
-
- int main() {
- int ret = func();
- std::cout << ret << std::endl;
- return 0;
- }
先看下A.cpp 编译后的产物

在看下main.cpp 编译后产物

很显然我们在main.o 和A.o中并没有看到 globl symbol符号,那么当然也不会出现 multipe definition定义,自然就会正常输出 .
3.3 变成两个 Weak Symbol 这个可以直接用编译器拓展 __attribute__((weak))
func 的副本. (具体选哪个要看链接器的实现了, 你要做的是确保每个副本是一样的)- // a.h
-
- #pragma once
-
- // 方式一:只声明
- // int func();
-
- // 方式二 : 添加static 变成 local symbol符号
- //static int func() {
- // // static修饰的 函数,会生成local symbol
- // return 1;
- //};
-
- // 方式二 : 匿名的 namespace里面都是 Local Symbal符号
- //namespace {
- // int func() {
- // return 1;
- // }
- //}
-
- // 方式三 :添加 inline 修饰符,变成 Weak Symbol
- inline int function() {
- return 2;
- }
-
-
- // A.cpp
- #include
- #include"a.h"
- void printf_fun()
- {
- std::cout << function() << std::endl;
- }
-
-
- // main.cpp
- #include
- #include"a.h"
-
- int main() {
- int ret = function();
- std::cout << ret << std::endl;
- return 0;
- }

这三种处理方案, 一般使用 1, 3 是比较常见的. 第 2 种的话, 每一个翻译单元都有一份副本, 会增加最终生成二进制的体积和符号个数.
如果你在 struct/class/union 中给出了函数的完整定义, 那么它也是隐式 inline 的. 比如
- struct A {
- int func() { return 0; } // 隐式 inline, 所以不会有重定义的错误
- };
inline 语义大概就是: 允许同一个定义在不同的翻译单元出现, 但你需要确保不同翻译单元给出的定义是一致的. 可以看出, 最自然的实现方案就是使用 ELF 格式中的 Weak Symbol.