链接(linking)就是将不同部分的代码和数据收集和组合成为一个单一文件的过程,这个文件可被加载(或被拷贝)到存储器并执行。
链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时;也可以执行于加载时(load time),也就是在程序被加载其(loader)加载到存储器并执行时;甚至执行于运行时(run time),由应用程序来执行。在早期的计算机系统中,链接是手动执行的,在现代系统中,链接是由链接器(linker)的程序自动执行的。
------------------
main.c
↓ 预处理器cpp
main.i
↓ 编译器ccl
main.s
↓ 汇编器as
main.o
↓ 链接器ld
main.exe
------------------
静态链接器static linker以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的可以加载和运行的可执行目标文件作为输出。输入的可重定位目标文件由各种不同的代码和数据节section组成。指令在一个节,初始化的全局变量在一个节,而未初始化的变量又在另一个节。
为了创建可执行文件,连接器必须完成:
1.符号解析symbol resolution: 目标文件定义和引用符号。符号解析的目的是将每个符号引用和一个符号定义联系起来。
2.重定位relocation,编译器和汇编器生成从地址零开始的代码和数据节。链接器通过把每个符号定义与一个存储器位置联系起来,然后修改所有对这些符号的引用,使得他们指向这个存储器位置,从而重定位这些节。
目标文件
目标文件有3种形式:
1. 可重定位目标文件。包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件。
2. 可执行目标文件。包含二进制代码和数据。其形式可以被直接拷贝到存储器并执行。
3. 共享目标文件。一种特殊类型的可重定位目标文件,可以在加载或运行时,被动态地加载到存储器并链接。
编译器和汇编器生成可重定位目标文件。连接器生成可执行目标文件。从技术上说,一个目标模块object module就是一个字节序列,而一个目标文件object file就是一个存放在磁盘文件中的目标模块。
各个系统的目标文件格式都不一样。贝尔实验室的第一个Unix系统使用的是.out。 System V Unix的早期版本使用的是COFF,Common Object File Format。windows使用的是COFF的一个变种,叫做PE,portable executable。现代Unix和Linux使用的是Unix ELF,Executable and Linkable Format.
可重定位目标文件
一个ELF可重定位目标文件:
---------------- 0
ELF头 描述字的大小/字节顺序/目标文件类型/机器类型
----------------
.text 已编译程序的机器代码
----------------
.rodata 只读数据,比如printf中的格式串和switch语句的跳转表
----------------
.data 已初始化的全局c变量。(局部C被保存在栈中)
----------------
.bss 未初始化的全局C变量。仅仅是占位符,不占用实际磁盘空间。
block storage start, better save space
----------------
.symtab 符号表,存放在程序中被定义和引用的函数和全局变量信息。
不同于编译器的符号表,这里不包含局部变量的表
----------------
.rel .text 当链接器把这个目标文件和其他文件结合时,text节中的许多位置都需要修改。
一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。调用本地函数
的指令不需要修改。注意,可执行目标文件中并不需要重定位信息,因此通常省略。
----------------
.rel .data 被模块定义或引用的任何全局变量的信息。
任何已初始化全局变量的初始值是全局变量或者外部定义函数的地址都需要修改。
----------------
.debug 测试符号表
----------------
line 原始c中的行号和.text中机器指令之间的映射。
----------------
.strtab 一个字符串表,包括.symtab和.debug中的符号表,以及节头中的节名字。
字符串表就是以null结尾的字符串序列。
----------------
节头部表
----------------
符号和符号表
每个可重定位目标模块m都有一个符号表,它包含m所定义和引用的符号的信息。在链接器的上下文中,有三种不同的符号。
1. 由m定义并能被其他模块引用的全局符号。全局链接器符号对应于非静态的C函数以及被定义为不带C的static属性的全局变量。
2. 由其他模块定义并被m引用的全局符号。 这些符号被称为外部符号external,对应于定义在其他模块中的c函数和变量。
3. 只能被模块m定义和引用的本地符号。有的本地链接器符号对应于带static属性的c函数和全局变量。
注:c中的static属性,实际上是在隐藏变量和函数声明。任何不声明为static的全局变量和函数都是公共的。
符号解析
链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义联系起来。对那些和引用定义在相同模块中的本地符号的引用,符号解析式非常简单明了的。编译器只允许每个模块中的每个本地符号只有一个定义。编译器还确保静态本地变量,他们也会有本地链接器符号,拥有唯一的名字。
对全局符号的解析。当编译器遇到一个不是在当前模块中定义的符号时,他会假设该符号在其他某个模块中,生成一个链接器符号表标目,并把它交给链接器处理,如果链接器在任何输入模块中都找不到这个被引用的符号,他就输出一条错误信息,并终止。
对全局符号的解析一个更麻烦的问题是,相同的符号可能会被多个目标文件定义。
注:c++和java中的重载函数,编译器是使用函数名和参数列表组合成对链接器来说唯一的名字。这种编码过程叫毁坏mangling,相反的过程叫恢复demangling。
在编译时,编译器输出每个全局符号给汇编器,或者是strong,或者是weak。汇编器把这个信息隐含地编码在可重定位目标文件的符号表里。函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。对同名的符号链接器选择时有三条规则:
1. 不允许同名的强符号;
2. 如果一个强符号和多个弱符号,则选强符号;
3. 如果多个弱符号,则任意选一。(这里不会出现编译时的错误信息,非常危险)
静态库
所有的编译系统都提供一种机制,将所有相关的目标模块打包为一个单独的文件,成为静态库static library,它也可以做链接器的输入。当链接器构造一个输出时,它只拷贝静态库中被应用程序引用的目标模块。
共享库
shared library是致力于解决静态库缺陷的一个现代创新产物,也称为共享目标.shared object. unix中通常用.so表示。windows中的共享库,就是dll,动态链接库。