链接器工作原理
链接器解析符号
链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来,可重定位目标文件的符号表在随笔ELF可重定位目标文件 - mjy66 - 博客园 (cnblogs.com)中有提到,以ELF格式的目标文件举例,.symtab节就是其符号表。
在解析符号的过程中,编译器针对局部符号和全局符号有不同的规则。在解析局部符号的过程中,编译器只允许每个模块中每个局部符号有一个定义,而对于全局符号的解析,若遇到一个不是在当前模块中定义的符号,编译器会假设该符号是在其它模块中定义,生成链接器符号表条目,若后续链接器在任何输入模块中都找不到被引用的符号定义,则会报错。
1、链接器如何解析多重定义的全局符号
链接器的输入是一组可重定位目标模块,每个模块有定义自己的一组符号,有些是局部(只对定义该符号的模块可见),有些是全局的(对其他模块也可见)。若多个目标模块定义了同名的全局符号,在Linux系统中,汇编器会以强符号或者弱符号来标记每个全局符号,函数和已初始化的全局变量为强符号,未初始化的全局变量为弱符号。Linux会根据以下规则处理多重定义的符号名:
- 规则1:不允许有多个同名的强符号
- 规则2:如果有一个强符号和多个弱符号同名,则选择强符号
- 规则3:如果有多个弱符号同名,则从这些弱符号中任意选择一个。
例1:
//main.c
int x = 1000;
int main()
{
return 0;
}
//f.c
int x = 1000;
void f()
{
}
main.c文件中定义并初始化了一个全局变量x,f.c文件中也定义并初始化了一个全局变量x,将这两个文件放在一起编译,会违反第一条规则,出现了两个同名的强符号,因此会出现如下报错。
例2:
//main.c
int x = 1000;
int main()
{
printf("x = %d\n",x);
return 0;
}
//f.c
int x;
void f()
{
}
若不对f.c文件中x进行初始化,则f.c中的全局变量x变成了弱符号,此时根据规则2,会优先选择main.c文件中的强符号,因此编译之后,运行就会出现如下结果:
2、静态库的链接
假设链接器不是读取一组可重定位目标文件,而是将所有相关的目标模块打包成一个单独的文件再作为链接器的输入,当链接器构造一个输出的可执行文件的时候,它只复制这个单独文件里被应用程序引用的目标模块,这个单独的文件就是静态库。静态库的出现能够在节省计算机内存的情况下,方便程序员调用相关函数。
在linux系统中,静态库以存档的特殊文件格式存放在磁盘中,存档文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和位置,存档文件名用后缀.a标识。
实践能够让我们对知识点的理解更加深刻,接下来将使用AR工具创建一个自己简单的静态库。创建静态库的步骤如下:
1、编写源文件
我们先创建三个分别对向量进行加减乘操作的文件,并在每个文件中记录被调用的次数。三个文件例程如下所示:
//addvec.c
int addcnt = 0;
void addvec(int *x,int *y,int n)
{
int i;
addcnt++;
for(i = 0;i < n; i++)
y[i] = x[i] + y[i];
}
//subvec.c
int subcnt = 0;
void subvec(int *x,int *y,int n)
{
int i;
subcnt++;
for(i = 0;i < n; i++)
y[i] = y[i] - x[i];
}
//multvec.c
int multcnt = 0;
int multvec(int *x,int *y,int n)
{
int i;
multcnt++;
for(i = 0;i < n; i++)
y[i] = y[i] * x[i];
}
2、创建文件之后,在shell命令行中输入以下命令:
[root@master test]# gcc -c addvec.c subvec.c multvec.c
[root@master test]# ar rcs libvec.a addvec.o multvec.o subvec.o
上述命令运行完成之后,便会输出libvec.a存档文件:
3、编写一个应用程序来调用这个库里的函数,同时也要写一个声明静态库中函数或变量的头文件,应用程序如下:
#include <stdio.h>
#include “vector.h” //声明静态库里的函数和全局变量
int x[2]={1,2};
int y[2]={3,4};
int z[2]={5,6};
int main()
{
addvec(x,y,2);
printf("y = [%d %d]\n",y[0],y[1]);
subvec(z,y,2);
printf("y = [%d %d]\n",y[0],y[1]);
multvec(y,y,2)
printf("y = [%d %d]\n",y[0],y[1]);
printf("addcnt = %d\n",addcnt);
printf("subcnt = %d\n",subcnt);
printf("multcnt = %d\n",multcnt);
return 0;
}
4、在编译的时候,链接时带上自己编写的库,在shell命令行中输入的命令如下:
[root@master test]# gcc main.c -L. -lvec
//-lvec参数是libvec.a的缩写
//-L.参数告诉链接器在当前的目录下查找libvec.a的缩写
命令运行之后,会生成一个a.out文件,在命令行中输入运行该可执行文件,输出:
3、链接器如何使用静态库来解析引用
在符号解析阶段,链接器会维护三个集合:
- 集合E:储存可重定位目标文件,这些文件最终会被合并起来形成可执行文件
- 集合U:储存未解析的符号,也就是引用了但是未定义的符号
- 集合D:储存前面输入文件中已经定义的符号
首先,命令行上的每个输入文件f,链接器会判断f是目标文件还是存档文件,如果f是目标文件,则将f添加到E,并通过修改U和D来反映f中的符号定义和引用。若f是存档文件,则链接器尝试匹配U中未解析的符号,若某个存档文件成员m定义了一个符号解析U中的一个引用,则将该存档文件成员加到E中,并根据该存档文件成员中的符号定义和引用来修改U和D,任何不包含在E中的成员目标文件都会被丢弃。循环反复以上过程,直到扫描完所有的输入文件,若最终U是非空的,则输出一个错误并终止,否则就合并E中的目标文件,构建输出的可执行文件。
从上述过程中可以看出链接器对输入的文件的处理有个先后的过程,若在输入命令的时候不加注意,就会出现报错,假如将一个库文件放在调用该库的应用文件前,此时链接器先处理库文件,由于U中还是空的,因此直接跳过库文件,直接处理应用文件,显然应用文件中的符号不会得到匹配。
补充:
1、在生成自己的链接库的时候,按照CSAPP的命令行输入:
gcc -static -o prog main.c libvector.a
会出现/usr/bin/ld:找不到-lc的报错。
原因:在新版的Linux系统下安装gcc的时候,不会安装libc.a,只会安装libc.so,所以当加上-static选项时,找不到libc.a就报错找不到libc了
解决方法:安装glibc-static
参考链接:/usr/bin/ld: cannot find -lc错误原因及解决方法-CSDN博客
2、C++和Java中链接器如何区别重载函数之间的区别
重载函数在源代码中都有相同的名字,但是有不同参数列表,编译器将每一个方法和参数列表编码成对链接器来说唯一的名字,这种编码的过程叫做重整。对类来说,重整的类名字是类名字中字符的整数数量+原始名字,比如类Foo被重整为3Foo。对方法来说,方法被编码为原始方法名+__
+被重整的类名+每个参数的单字母编码,比如Foo::bar(int,long)
被编码为bar__3Fooil