主要任务:
1.符号解析
在声明变量和函数之后,所有的符号声明都被保存到符号表。
而符号解析阶段会给每个符号一个定义。
2.重定位:
把每个符号的定义与一个内存位置关联起来,然后修改所有对这些符号的引用,让他们
指向内存位置。
符号解析:
会将符号引用和可重定位目标文件的符号表中的确定符号定义关联起来。
对于相同模块的局部符号(static属性),可以简单解决。
对于全局符号的引用,如果不是在当前模块定义的,会假设在其它模块中,然后通过链接器去查找,找不到则报错。
重载函数:
对于重载函数,会将每个函数和参数列表组合成一个对链接器来说唯一的名字。
多重定义的全局符号:
函数和已初始化的全局符号是强符号,未初始化的全局变量是弱符号。
如果有一个强和多个弱,那么选择强符号;若是多个弱符号,则随机选一个;不允许多个强符号。
/*foo1.c*/
int x = 15151;
int main
{}
/*foo2.c*/
int x = 15151;
void f(){}
如上面的代码会报错,x被多次定义。
/*foo1.c*/
int x = 15151;
int y = 111;
void f();
int main
{}
/*foo2.c*/
double x ;
void f()
{
x = 0.0;
}
double是8字节,int是4字节。因为强弱符号的关系,x=0.0中的x是foo的,于是双精度浮点会覆盖掉x,y的位置。
静态库:
相关的函数会被编译成独立的目标模块,然后封装成一个独立的静态库文件。
在链接的时候,连接器只会复制被程序引用的目标模块,以减小可执行文件的大小。
--
如何解析静态库:
链接器在根据命令行中输入的可重定位目标文件和静态库的顺序从左到右的扫描这些文件。然后维护三个集合:
E:会被合并的文件
U:未解析的符号集合
D:已经定义的文件集合
根据命令行的输入,判断f是目标文件还是存档文件
- 目标文件:链接器会把f加入E,然后修改U,D。
- 存档文件:在U中进行匹配,如果匹配到了,将存档文件成员 添加到E,然后修改UD。
--
如果不使用静态库:
1.将所有标程放到一个目标模块中
但是这样会导致每个可执行文件都包含所有的函数副本,浪费空间。而且任何函数被修改了,都要
重新编译库整个源文件。
2.将所有标程放到一个目录下
可以解决空间浪费的问题,但是这样需要我们显示地链接合适的目标模块,如果函数过多,容易出现错误,而且
浪费时间。
重定位:
1.重定位会将所有同一类型的合并为聚合节。然后将运行时地址赋予它们
2.修改代码节和数据节中对每个符号的引用,使它们指向正确的地址(相对引用 or 绝对引用重定位 等等)。
共享库:
静态库也有一定缺陷,需要定期维护更新。
几乎每个C都会使用IO,那么printf和scanf会被复制到每个运行进程的文本。那么在一个运行了上百个进程的系统上,无疑是一种浪费。
共享库不是像静态库那样将代码和数据嵌入到执行文件中,而是在内存中有一个共享库的.text节的副本,可以被不同的正在运行
的进程共享。
--
在共享库下,创建一个可执行程序:
会先静态执行一些链接,然后在程序加载的时候,动态地完成链接过程。
在加载运行的时候,会主要到.interp节(有动态链接器的路径)。然后加载这个动态链接器
- 重定位共享库的数据文本到某个内存段
- 重定位执行文件中所有对由 共享库定义的符号的引用也可以通过dlopen在应用程序中加载和链接共享库
参考资料:
不周山之读薄CSAPP
《深入理解计算机系统 》
多个进程间共享动态链接库原理