-
链接
-
定义
将不同部分的代码和数据,收集和组合成为一个单一文件的过程。这个文件可以被加载或拷贝到存储器并执行。
-
发生时刻
(1) 编译(compile time)
源代码被翻译成机器代码
(2) 加载(load time)
程序被加载器load到存储器并执行时
(3) 运行(run time)
应用程序执行
-
链接由链接器(linker)的程序完成,作用是分离编译,把每个模块单独进行编译,然后再链接在一起
-
-
编译器驱动程序
-
编译器驱动程序包括:
(1) 预处理器
(2) 编译器
(3) 汇编器
(4) 链接器
-
示例
现在有两个c文件: main.c 和 swap.c, main.c中有 main() 函数,调用swap.c中的 swap() 函数
在 shell 下输入:
> gcc -02 -g -o p main.c swap.c
过程:
(1) main.c ---- c预处理器 ----> main.i main.i 是一个ASCII码的中间文件 (2) main.i ---- c编译器(ccl) ----> main.s main.s 是一个ASCII码的汇编文件 (3) main.s ---- 汇编器(as) ----> main.o main.o 是一个可重定位目标文件 同理, swap.c经过上述三个预处理、编译、汇编过程,也得到了 swap.o 目标文件 (4) main.o + swap.o ---- 链接器(ld) ----> p p 是一个可执行文件
当在 shell 下输入 > ./p 时,shell 会调用 os 中的加载器函数,把 p 的代码和数据拷贝到存储器,然后将控制转移到程序的开头
-
-
静态链接
-
目标文件是字节块的集合,这些字节块可能包含
(1) 程序代码
(2) 程序数据
(3) 指导链接器和加载器的数据结构
-
链接器的任务
(1) 符号解析
将每个符号引用和一个符号定义联系起来
(2) 重定位
编译器和汇编器将每个源文件都生成从零开始的代码和数据节,链接器把每个符号定义与一个存储器位置联系起来
-
链接器对目标机器的了解甚少,编译器和汇编器完成了大部分工作
-
-
目标文件
包括三种
-
可重定位目标文件
包含二进制代码和数据,可以和其它可重定位目标文件合并起来,创建可执行目标文件(例如上面的main.o、swap.o这种)
-
可执行目标文件
包含二进制代码和数据,可以直接被拷贝到存储器执行
-
共享目标文件
在 load 或者 run time 时,被动态加载到存储器并链接
-
-
可重定位目标文件
-
elf 是 unix 的目标文件
-
elf 结构
.text: 已编译程序的机器代码
.rodata: 只读数据(例如字符串)
.data: 已初始化的全局 C 变量
.bss: 未初始化的全局 C 变量
.symtab: 在程序中被定义和引用的 函数 + 全局变量 的信息
…
注:1. 局部 C 变量运行时被保存在栈中,既不在.data中,也不在.bss中
- 带有 static 的 局部C变量放在 .data 或 .bss 中
-
-
符号和符号表
-
符号分类
(1) 全局符号
由当前模块定义,并且能被其他模块引用的符号
(2) 外部符号
由其他模块定义,并且被当前模块引用的符号
(3) 本地符号
只被当前模块定义和引用的符号
-
C 语言的规则
(1) 任何声明 带有static 的全局函数和全局变量,都是模块私有的
(2) 任何 不带static 声明的全局函数和全局变量,都是公共的
-
-
符号解析
-
作用:
将每个引用和它输入的可重定位目标文件的符号表的一个确定的符号定义联系起来
-
当编译器遇到一个不是在当前模块中定义的符号(变量或函数名),它会假设这个符号在其他模块定义,然后交给链接器处理;当链接器也找不到的时候,就会报一个链接错误
-
强符号和弱符号
(1) 强符号
函数
已经初始化的全局变量
(2) 弱符号
未初始化的全局变量
-
多处定义的全局符号的处理规则
(1) 不允许有多个强符号
(2) 如果有1个强符号和多个弱符号,那么选择强符号
(3) 如果有多个弱符号,那么从中任意选择一个
-
静态库
(1) 所有的目标模块可以打包成一个单独的文件,这个文件也可以和其他可重定位目标文件一样,作为链接器的输入,这个文件就叫做静态库文件
(2) 静态库是一系列模块的集合。当链接器构造可执行文件时,它只需要拷贝静态库中用到的那些模块。
(3) 示例
1° libvector.a 是一个静态库文件 2° vector.h 定义了 libvector.a 中的函数原型(**这其实还起到了隐藏源代码的作用**) vector.h #ifndef PLAY_GROUND_VECTOR_H #define PLAY_GROUND_VECTOR_H void addvec(int* x, int* y, int* z, int n); void multvec(int* x, int* y, int* z, int n); #endif //PLAY_GROUND_VECTOR_H 3° libvector.a 中由两个模块集合而成,这两个模块的源文件是 addvec.c 和 multvec.c addvec.c void addvec(int *x, int *y, int *z, int n) { for (int i = 0; i < n; i++) { z[i] = x[i] + y[i]; } } multvec.c void multvec(int *x, int *y, int *z, int n) { for (int i = 0; i < n; i++) { z[i] = x[i] * y[i]; } } 4° main.c 中只需要 include vector.h 这个头文件得到函数原型,然后在链接的时候把 libvector.a 用到的模块链接进去即可 main.c #include <stdio.h> #include "./vector.h" int x[2] = {1, 2}; int y[2] = {3, 4}; int z[2]; int main() { printf("lib example\n"); addvec(x, y, z, 2); printf("z = [%d %d]\n", z[0], z[1]); return 0; }
(4) 链接器使用静态库解析引用的过程
1° 链接器按照在命令行上出现的顺序,从左到右扫描 2° 扫描到可重定位目标文件时,符号引用先占个位;扫描到静态库时,把符号引用和符号定义联系起来 3° 因此,如果各个静态库不是独立的,它们必须进行**排序**,使得至少一个符号定义出现在符号引用之后
(5) 一旦链接器完成符号解析,每个符号引用和符号定义就都联系了起来,链接器知道它的输入目标模块中的代码节和数据节的确切大小
-
-
重定位
-
重定位包括2个步骤
(1) 重定位节和符号定义
将所有模块中相同的节合并为同一类型的新的聚合节
(2) 重定位节的符号引用
修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行地址
这又分为绝对地址和PC的相对地址
-
-
可执行目标文件
-
二进制文件
-
包含加载程序到存储器并运行它所需要的所有信息
-
-
动态链接共享库
-
静态库的缺陷是:
(1) 需要定期维护和更新,然后用到静态库的所有可重定位目标文件都要重新链接
(2) 相同的部分(例如printf)会被复制到每个进程的文本段中
共享库就是为了解决静态库的缺陷而诞生的
-
共享库在运行时可以加载到任意的存储器地址,并在存储器中和一个程序链接起来
-
(1) 一个共享库只有一个.so(.dll)文件,所有引用该库的可执行目标文件共享这个.so文件的代码和数据,而不是分别拷贝和嵌入
(2) 一个共享库的.text段(已编译程序的机器代码)只有一个副本,可以被不同的正在运行的进程共享
-
动态链接的过程
(1) 创建可执行文件时,静态执行一部分链接
注:这部分拷贝的是一些重定位和符号表信息,不会拷贝数据节和代码节
(2) 程序加载时,动态完成完全链接
注:此时共享库的位置已经固定
-
JNI(Java Native Interface)的原理
将本地C函数先生成共享库,然后JVM动态链接和加载动态库
-