程序员的自我修养—目标文件里有什么
第三章的后半部分
链接的接口
- 链接的本质是要把多个不同的目标文件之间相互"粘"在一起。为了使不同的目标文件可以相互粘合,这些目标文件之间必须有固定的格式。
- 在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。
- 我们可以将符号看作是链接中的粘合剂,整个链接过程中正是基于符号才能正确完成。链接过程中很关键的一部分就是符号的管理,每一个目标文件都会有一个相应的符号表(Symbol Table),这里面记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值(Symbol Value),对于变量和函数来说,符号值就是他们的地址。首先对符号进行一定的分类:
- 定义在本目标文件的全局符号,可以被其他目标文件引用。
- 在本目标文件中引用的全局符号,却没有定义在本目标文件,这一般被叫做外部符号(External Symbol)。
- 段名,这种符号往往由编译器产生,它的值就是该段的起始地址。
- 局部符号,这类符号只在编译单元内部可见。调试器可以使用这些符号来分析程序或崩溃时的核心转储文件。这些局部符号对于链接过程没有作用,链接器往往也忽略它们。
对于我们来说,最值得关注的是全局符号。使用 nm xxx.o 来查看符号表。

ELF 符号表结构
ELF中的符号表往往是文件中的一个段,段名为 “.symtab”。符号表的结构很简单,它是一个 Elf32_Sym 结构(32位ELF文件)的数组,每个 Elf32_Sym 结构对应一个符号。这个数组的第一个元素是无效的未定义符号,结构定义如下:
1 | typedef struct |
| st_name | 符号名,这个成员包含了该符号在字符串表中的下标 |
| st_value | 符号相对应的值,这个值和符号有关 |
| st_size | 符号的大小,对于包含数据的符号,这个值是该数据类型的大小。如 double 8字节,但若为0,则表示符号大小为0或未知 |
| st_info | 符号类型和绑定信息 |
| st_other | 0,目前无意义 |
| st_index | 符号所在的段 |
符号类型和绑定信息
-
低4位表示符号的类型(Symbol Type),高28位表示绑定信息(Symbol Binding)
-
符号绑定信息
| 宏定义名 | 值 | 说明 |
|---|---|---|
| STB_LOCAL | 0 | 局部符号 |
| STB_GLOBAL | 1 | 全局符号 |
| STB_WEAK | 2 | 弱引用 |
- 符号类型
| 宏定义名 | 值 | 说明 |
|---|---|---|
| STT_NOTYPE | 0 | 未知类型符号 |
| STT_OBJECT | 1 | 该符号是数据对象,如变量,数组 |
| STT_FUNC | 2 | 该符号是一个函数或其他可执行代码 |
| STT_SECTION | 3 | 该符号表示一个段 |
| STT_FILE | 4 | 该符号表示文件名,一般都是该目标文件对应的源文件名 |
符号所在段
-
如果符号定义在本目标文件中,那么这个成员表示符号所在的段在段表中的下标;但是如果符号不是定义在本目标文件中,或者对于有些特殊的符号,sh_shndx 的值会有些特殊
-
符号表所在段特殊常量
| 宏定义名 | 值 | 说明 |
|---|---|---|
| SHN_ABS | 0xfff1 | 表示该符号包含一个绝对的值 |
| SHN_COMMON | 0xfff2 | 表示该符号是一个 COMMON 块,一般来说,未初始化的全局符号定义就是这种类型 |
| SHN_UNDEF | 0 | 该符号未定义。这个符号表示该符号在本目标文件中被引用到但定义在其他目标文件中 |
符号值
- 在目标文件中,如果是符号的定义并且该符号不是 “COMMON” 块类型,则 st_value 表示该符号在段中的偏移。即符号所对应的函数或变量位于由 st_shndx 指定的段,偏移 st_value 的位置。这也是目标文件中定义全局变量的符号最常见的情况,如 main, global_init_var
- 在目标文件中,如果符号是 “COMMON” 块,则 st_value 表示该符号的对齐属性,比如 simple_section.o 中的 global_uninit_var
- 可执行文件中,st_value 表示符号的虚拟地址。这个虚拟地址对于动态链接器很有用
可以使用 readelf -s xxx.o
1 | readelf -s simple_section.o |
- 对于未显示的 STT_SECTION 类型的符号,它们表示下标为 Ndx 的段的段名,他的符号名就是段名,如 Ndx 为1应为 .text 段
- static_var 变成了 static_var.1419,这和符号修饰有关
- simple_section.c 表示编译单元的源文件名
特殊符号
-
当我们使用 ld 作为链接器来链接生成可执行文件时,它会为我们定义很多特殊的符号,这些符号并没有在你的程序中定义,但你可以直接声明并且引用它,我们称之为特殊符号。链接器会在将程序最终链接成可执行文件的时候将其解析为正确的值。只有用 ld 链接生产最终可执行文件的时候这些符号才会存在。
- __exectuable_start: 程序的起始地址,并非入口地址,是程序最开始的地址
- __etext & _etext & etext: 代码段结束地址,即代码段最末尾的地址
- _edata & edata: 该符号表示数据段结束地址,即代码最末尾的地址
- _end & end: 程序结束的地址
符号修饰和函数签名
- 最早的时候,编译器编译源代码产生目标文件时,符号名与相应的变量名或者函数名是一致的。但后来 UNIX 平台和 C 语言出现,一个 C 程序想要使用原来用汇编写的库或者目标文件时,就不能用和这些库中定义的函数和变量名字重复的,否则会造成冲突。
- 为了防止符号名冲突,UNIX 下的 C 语言规定, C 语言源代码文件中的所有全局变量和函数经过编译以后,相应符号名前加上下划线。别的语言也加上别的东西。这是最原始的解决方法。后来便有了命名空间(NameSpace) 的方法来解决多模块的符号冲突问题。
C++ 符号修饰
1 | int func(int); |
区别他们,用到了一个叫做**函数签名(Function Signature)**的东西,函数签名包括了一个函数的信息,如:参数类型,类,函数名,返回值等。这样每个函数名虽然相同,但其函数签名不同,我们用某种名称修饰的方法使得每个函数签名对用一个修饰后的名字。
上述函数在 gcc 编译器下相对应的修饰后的名称:
| 函数签名 | 修饰后的符号名 |
|---|---|
| int func(int) | _Z4funci |
| float func(float) | _Z4funcf |
| int C::func(int) | _ZN1C4funcEi |
| int C::C2::func(int) | _ZN1C2C24funcEi |
| int N::func(int) | _ZN1N4funcEi |
| int N::C::func(int) | _ZN1N1C4funcEi |
我们不需要了解怎么修饰, binutils 提供了一个 “c++filt” 的工具可以解析
1 | $ c++filt _ZN1N1C4funcEi |
- 当然 Visual C++ 修饰的方法和结果与 gcc 完全不同。
- 由于不同的编译器采取不同的的名字来修饰,必然导致不同的编译器直接的文件无法正常相互链接,这是导致不同编译器之间不能互操作的重要的原因之一
extern “C”
- C++ 为了与 c 兼容,在符号管理方面,使用 extern “C” 来把作用的代码当做 C 的代码来处理。
- 很多时候,我们会碰到有些头文件声明了一些 C 语言的函数和全局变量,但是这个头文件可能会被 C 语言的代码或 C++ 代码包含。如 string.h 库中的 memset 函数。如果不做任何处理,在 C++ 中就会认为其是 C++ 的函数,这样修饰后就无法和 C 语言库的 memset 符号进行链接。对于 C++,必须使用 extern “C” ,但 C 语言没有这个。
- 解决方法:使用 C++ 的宏 “__cplusplus”
1 | // 使用条件宏来判断当前编译单元是不是 C++ 的代码 |
- 如果是 C++ 会加上 extern “C” 来声明。如果是 C 代码就直接声明。上面这段代码的技巧几乎在所有的系统头文件里都被用到。
强符号和弱符号
- 全局初始化的变量会被称为强符号(Strong Symbol)。而未初始化则被认为是弱符号(Weak Symbol)
- 我们可以用 GCC 的 “__attribute__((weak))” 来定义任何一个强符号为弱符号。需要注意:强符号和弱符号都是相当于定义来说的,而不是针对符号的引用。针对强弱符号的概念,链接器按如下规则处理
- 不允许强符号被多次定义
- 如果一个符号在某个目标文件中是强符号,在其他文件中是弱符号,则选择强符号作为定义
- 如果一个符号在多个目标文件中都是弱符号,则选择占用空间最大的
弱引用和强引用
- 对外部目标文件的符号引用在目标文件被最终链接时要被正确的决议。如果找不到定义,则报未定义错误,这种被称为强引用(Strong Reference)。与之对应还有弱引用(Weak Reference),在处理弱引用时,如果有定义,则用定义。如果没有则对于该引用不报错。一般对于未定义的弱引用,链接器默认其为0。
1 | __attribute__((weak)) void foo(); |
执行一下没有任何结果,果然 foo 被赋值为0
关于 weakref
weakref
weakref (“target”)
The weakref attribute marks a declaration as a weak reference. Without arguments, it should be accompanied by an alias(别名) attribute naming the target symbol. Optionally, the target may be given as an argument to weakref itself. In either case, weakref implicitly(隐式) marks the declaration as weak. Without a target, given as an argument to weakref or to alias, weakref is equivalent to weak.
static int x() __attribute__ ((weakref (“y”)));
/* is equivalent to… /
static int x() __attribute__ ((weak, weakref, alias (“y”)));
/ and to… */
static int x() __attribute__ ((weakref));
static int x() __attribute__ ((alias (“y”)));
A weak reference is an alias that does not by itself require a definition to be given for the target symbol. If the target symbol is only referenced through weak references, then it becomes a weak undefined symbol. If it is directly referenced, however, then such strong references prevail, and a definition is required for the symbol, not necessarily in the same translation unit.
At present, a declaration to which weakref is attached can only be static.
调试信息
- 如果在 gcc 编译时加上 -g 参数,就会加上调试信息。
- 调试信息有相应的标准格式而且往往很大(比程序的代码和数据本身大好多倍)
程序员的自我修养—目标文件里有什么