特殊符号
当我们使用ld作为连接器来链接生产可执行文件时,它会为我们定义很多特殊符号,这些符号没有在程序中定义,只有使用ld 链接生产最终可执行文件的时候这些符号在会存在 以下为几个具有代表性的特殊符号
__executable_start, 该符号为程序其实嗲之,注意不是入口地址,是程序的最开始的地址__etext或_etext或etext,该符号为代码段结束地址,即代码段最末尾的地址_end或end, 该符号为程序结束地址
以上地址都为程曦被装在时的虚拟地址
我们可以在程序中直接使用这些符号
#include <stdio.h>
extern char __executable_start[];extern char etext[], _etext[], __etext[];extern char end[], _end[]
int main(void){ printf("%x", __executable_start ); printf("%x", etext ); printf("%x", end );
return 0;}符号修饰与函数签名
为了防止类似的符号名冲突,UNIX下的C语言规定,C语言源代码文件中所有全局变量和函数在编译之后,相对应的符号名前加上下划线 ”_”.
但是随着时间的推移,很多操作系统和编译器被完全重写了好几遍,在现在的LIUNX下的GCC 编译器中,默认情况下已经去掉了C语言前加 ”_” 这种方式,但是 Windows 平台下的编译器还保持着这养的传统
C++ 符号修饰
C++拥有雷,继承,虚机制,重载,命名空间等这些设定,,为了支持这些复杂的特性,人们发明了* 符号修饰或符号改编* 的机制
int func(int);class C{public: C() {} ~C() {}
int func(int);
class C2 { float func(float); };};
namespace N{ int func(int); class C { int func(int); }}这段代码拥有若干个同名函数func,只不过他们的返回类型和参数以及所在的名称空间不同,我们引入一个属于叫做 函数签名,函数签名包含了一个函数的信息,包括函数名。他的参数类型,他所在的类和名称空间以及其他星系。函数签名用于识别不同的函数,就像签名用于识别不同的人一样,函数的名字只是函数签名的一部分。由于上面的几个同名函数的参数类型以及所处的类和名称不同,我们可以认为他们的函数签名不同。在来编译器以及链接器处理富豪的时候,他们使用某种名称修饰的方法使得每一个函数签名都对应一个 修饰后名称 。所以编译器在将C++ 源代码编译成目标文件的时候会昂函数变量的名字进行修饰
上面几个函数在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 |
GCC 的基本C++名称修饰如下所有符号都以_Z 开头,对于嵌套的名字,后后面紧跟 N 然后 是各个名称空间和类的名字每个名字是名字字符串的长度在以E结尾他的参数紧跟E的后面
但是不同编译器厂商的名称修饰方式可能不同,所以不同编译器对于同一个函数签名可能对应不同的修饰后名称
但是 C++ 为了与C 兼容,在符号的管理上 C++ 有一个用来声明或者定义C的符号 extern 'C'的关键字用法
extern "C"{ int func(int); int var;}C++ 编译器会将 这个 花括号内部的代码当作C语言处理
弱符号与强符号
对于 C/C++ 来说 编译器默认函数和初始化了的全局变量为强符号,而未初始化的变量为弱符号 我们也可以通过
int weak = 1;__attribute__((weak)) weak2 = 2;针对强弱符号的概念,连接器就会按照如下规则处理与选择多次定义的全局符号
- 不允许强符号被多次定义(即不同的目标文件中不能有同名的强符号)如果有多个强符号定义,则链接器报符号重复定义错误,那么选择强符号
- 如果一个符号在所有目标文件中都是强符号,在其他文件中都是若符号,那么选择强符号,那么选择强符号
- 如果一个符号在所有目标文件中都是若符号,那么选择其中占用文件最大的一个。eg:如果目标文件A定义的全局global为int型;目标文件B定义global 为double型 占8个字节,那么A与B链接后 符号global占用8个字节,尽量不要使用多个不同类型的弱符号,否则容易导致很难发现程序错误
弱引用和强引用
目前我们所看到的外部目标文件的符号引用在目标文件被最终链接成可执行文件,我们需要被正确决议,如果没有找到这个符号的定义,连接器就会报符号未定义错误,这种被称为强引用 与之相对的还有一种弱引用 ,如果该符号有定义,则该符号符号的引用决议
总结与对比
| 维度 | 强/弱符号 | 强/弱引用 |
|---|---|---|
| 核心问题 | 解决定义太多的冲突。 | 解决定义没有的容错。 |
| 关注点 | 谁的定义是有效的? | 找不到定义怎么办? |
| 默认情况 | 普通定义是强符号 。 | 普通引用是强引用 。 |
| 语法 | __attribute__((weak)) | __attribute__((weakref)) |
| 典型场景 | 库的默认实现,可被用户覆盖。 | 可选依赖、插件系统、向后兼容。 |
空间与地址的分配
含序叠加
直接将输入的目标文件按照次序叠加起来,但是这会导致一个问题,输出的文件将会有很多零散的段,比如一个规模稍大的应用程序,可能会有数百个目标文件,最后的输出文件将会有成百上千的零散的段,这种做法会浪费大量空间 段的装载地址和空间读对齐单位是页 4096个字节
相似段合并
一个更加实际的方式就是将相同性质的段合并到一起,比如将所有输出文件的.text 合并到输出文件的.text段,接着是 .data .bss 段等等
但是 .bss 段在目标文件和可执行文件中并不占用文件的空间,但是它在装载是占用地址空间,随意链接器在合并各个段的同时也将,bss 段合并
连接器为目标分配地址和空间的含义:
- 在输出的可执行文件中的空间;
- 第二个是在装在后的虚拟地址中的虚拟地址空间
两部链接
现在的链接器空间分配的策略基本上都才用相似段合并的方式,这种方法的连接器一般都采用一种叫做两步链接 的方法
- 空间与地址的分配 扫描所有的输入目标文件,并且获得他们的各个段的长度,属性和位置,并且将输入目标文件中的符号表和符号引用收集起来,统一放到一个全局符号表中,这一步中,连接器将能够获得所有输入目标文件的段长度,并且将它们合并,并计算出文件中各个段合并后的长度和位置,并建立映射关系
- 符号的解析与重定位 使用上面第一步中收集到的所有信息,读取输入文件中段的数据,重定位信息,并且进行符号解析与重定位,调整代码中的地址等。
假设有两个文件 a.o b.o 我们使用ld 将他们连接起来
ld a.o b.o -e main -o ab- -e main 表示将main函数作为程序的入口,ld连接器默认的程序入口为_start
- -o ab 表示链接输出的文件名字为ab, 默认为a.out
如图所示

VMA 表示 Virtual Memory Address, 即虚拟地址, LMA 表示 Load Memory Address, 即加载地址,正常情况下这两个值应该是一样的。
在链接之前,目标文件中的所有段地址已经是程序在进程中的虚拟地址,即我们关心上面各个段中的VMA 和Size,而忽略 文件偏移(File off)。我们可以看到,在连接之前目标文件中的所有段的VMA都是0 ,因为虚拟空间还没有分配,所以,他们默认都为0
在Liunx 下 ELF 文件可执行文件默认从地址 0x08048000 开始分配
符号地址的确定
我们还是以”a.o” 和 “b.o” 作为例子,来分析这两个步骤中连接器的工作过程。在第一步的扫描和空间分配阶段,链接器按照前面介绍的空间分配方法进行分配,这个时候输入文件中的各个段在连接后的虚拟地址就已经确定了
当前面一步完成之后,连接器开始计算各个符号的虚拟地址。因为各个符号在段内的相对位置是固定的,所以这时候其实”main” “shared” 和 swap 的地址也已经是确定的了,只不过连接器需要给每一个符号加上一个偏移量,是他们能够调整到正确的虚拟地址。
符号的解析与重定位
重定位
在完成空间和地址的分配步骤之后,链接器就进入了符号解析和重定位的步骤,这也是静态链接的核心内容,
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时










