注:本文内容摘自《深入理解计算机系统》
首先搞清楚什么是链接?链接是将各种代码和数据收集起来,并组合成为一个单一文件的过程。链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时;也可以执行于加载时(load time),也就是在程序被加载器(loader)加载到存储器并执行时;甚至执行于运行时(run time),由应用程序来执行。
链接是由叫做链接器(linker)的程序自动执行的。
认识编译驱动程序
编译系统提供编译驱动程序(compiler driver),它读取程序源文件并将其翻译成一个可执行的目标文件,这个过程分成四个阶段,分别是:
1. 预处理阶段(预处理器cpp)
2. 编译阶段(编译器ccl)
3. 汇编阶段(汇编器as)
4. 链接阶段(链接器ld)
我们结合以下程序来了解一个基本的”翻译”过程。
/*main.c*/
void swap();
int buf[2]={1,2};
int main()
{
swap();
return 0;
}
----------
/*swap.c*/
extern int buf[];
int *bufp0=&buf[0];
int *bufp1;
void swap()
{
int tmp;
bufp1=&buf[1];
tmp=*bufp0;
*bufp0=*bufp1;
*bufp1=tmp;
}
在这个示例程序中,main()函数调用swap()函数来交换全局数组buf中的两个数。
我们一步一步来,看看每一步到底发生了什么。
当前目录下,有main.c和swap.c两个文件
kkmjy@ubuntu:link$ ls
main.c swap.c
step1、运行预处理器(cpp)将.c文件翻译成中间文件(即.i文件)
kkmjy@ubuntu:link$ cpp main.c main.i
kkmjy@ubuntu:link$ cpp swap.c swap.i
经过step1之后,
kkmjy@ubuntu:link$ ls
main.c main.i swap.c swap.i
step2、运行编译器(ccl)将.i文件翻译成汇编语言文件(即.s文件)
(注:若此前没有安装过ccl,会跳出来这句话,按要求安装好就OK了)
The program 'ccl' is currently not installed. You can install it by typing:
sudo apt install cclive
ps:很奇怪,用ccl一直会出错,为了继续进行下去,用以下语句代替
kkmjy@ubuntu:link$ gcc -S main.i -o main.s
kkmjy@ubuntu:link$ gcc -S swap.i -o swap.s
经过step2后
kkmjy@ubuntu:link$ ls
main.c main.i main.s swap.c swap.i swap.s
step3、运行汇编器(as)将.s文件翻译成一个可重定位目标文件(即.o文件)
kkmjy@ubuntu:link$ as main.s -o main.o
kkmjy@ubuntu:link$ as swap.s -o swap.o
经过step3后
kkmjy@ubuntu:link$ ls
main.c main.i main.o main.s swap.c swap.i swap.o swap.s
step4、运行连接器(ld)将main.o和swap.o以及一些必要的系统目标文件组合起来,创建一个可执行目标文件p。
kkmjy@ubuntu:link$ ld -o p main.o swap.o
(注:用ld链接时可能会遇到cannot find entry symbol _start的问题,请移步Stack Overflow:load-warning-cannot-find-entry-symbol-start)
//TODO,由于网络问题,暂时还未搞明白这个问题。
至此,我们就分步完成了”翻译”的全部工作。
以上只是为了说明”翻译”的过程而做的演示,在Linux下,用gcc编译,就一句话:
gcc -o p main.c swap.c
以上过程如下图所示:
与静态库链接
迄今为止,我们都是假设链接器读取一组可重定位目标文件,并把它们链接起来,成为一个输出的可执行文件。实际上,所有的编译系统都提供一种机制,将所有相关的目标模块打包成为一个单独的文件,称为静态库(static library),它可以用作链接器的输入。在链接时,链接器将只拷贝被程序引用的目标模块,另一方面,应用程序员只需要包含较少的库文件的名字。在Unix系统中,静态库以一种称为存档(archive)的特殊文件格式存放在磁盘当中,存档文件是一组连接起来的可重定位目标文件的集合,有后缀.a标识。
下面是一个例子:
首先,我们自定义一个静态库,称它为libvector.a吧,该静态库由addvec.c和multvec.c生成。
/*addvec.c*/
void addvec(int *x,int *y,int *z,int n)
{
int i;
for(i=0;i<n;i++){
z[i]=x[i]+y[i];
}
}
------
/*multvec.c*/
void multvec(int *x,int *y,int *z,int n)
{
int i;
for(i=0;i<n;i++){
z[i]=x[i]*y[i];
}
}
我们使用ar工具来创建该库。
kkmjy@ubuntu:staticLib$ gcc -c addvec.c multvec.c //首先生成.o文件
kkmjy@ubuntu:staticLib$ ar rcs libvector.a addvec.o multvec.o //然后利用ar创建静态库
此时,便创建好了静态库libvector.a
kkmjy@ubuntu:staticLib$ ls
addvec.c addvec.o libvector.a multvec.c multvec.o
为了检测自定义的静态库是否work,写一个用例进行测试。如下:
/*vector.h*/
#ifndef VECTOR_H
#define VECTOR_H
void addvec(int *x,int *y,int *z,int n);
void multvec(int *x,int *y,int *z,int n);
#endif
----------
/*main.c*/
#include <stdio.h>
#include "vector.h"
int x[2]={1,2};
int y[2]={3,4};
int z[2]={0};
int main()
{
addvec(x,y,z,2);
printf("z = [%d %d]\n",z[0],z[1]);
return 0;
}
首先,我们故意不加静态库,看看会发生什么:
kkmjy@ubuntu:staticLib$ gcc -o main main.c
/tmp/ccz5fEFs.o: In function `main':
main.c:(.text+0x19): undefined reference to `addvec'
collect2: error: ld returned 1 exit status
可以看到,出现一个”undefined reference to xxx”的错误,而这个错误就是我们经常遇到的,究其原因,就是因为链接时链接器没有找到addvec()函数对应的定义,也就是没有加上静态库。现在,让我们加上所需的静态库。如下:
kkmjy@ubuntu:staticLib$ gcc -o main main.c libvector.a
至此,程序终于编译成功了。执行结果如下:
kkmjy@ubuntu:staticLib$ ./main
z = [4 6]
总结:
当链接器运行时,它判定addvec.o定义的addvec符号被main.o引用,所以它拷贝addvec.o到可执行文件。因为测试程序不引用multvec()函数,所以链接器不会拷贝这个模块到可执行文件。链接器还会拷贝libc.a中的printf.o模块,以及其他系统模块。下图概括了链接器的行为。
与动态库链接
使用静态库有以下两个缺陷:
1. 如果应用程序员想要使用一个库的最新版本,他们必须显示地将程序与更新了的库重新链接。
2. **在运行时,引用的静态库函数的代码会被复制到每个运行进程的文本段中,非常浪费存储器系统资源。**
而共享库可以解决静态库的这些缺陷。所有引用动态库的可执行目标文件,共享这个.so文件中的代码和数据,而不是像静态库那样被拷贝和嵌入到引用它们的可执行文件中。
(注:共享库(shared library),也称共享目标(shared object),在Unix系统中通常用.so后缀来表示,在Windows系统中称为dll(即动态链接库)。Anyway,它们表示的是同一个东西。)
同样用上面的例子,我们演示如何自定义动态库(libvector.so)并在程序中利用它。
首先,通过如下指令生成动态库
kkmjy@ubuntu:staticLib$ gcc -fPIC -shared -o libvector.so addvec.o multvec.o
然后,在编译main.c的加上libvector.so就好啦。
kkmjy@ubuntu:staticLib$ gcc -o p main.c libvector.so
此时,如果直接执行可执行文件p,会报错
kkmjy@ubuntu:staticLib$ ./p
./p: error while loading shared libraries: libvector.so: cannot open shared object file: No such file or directory
这是因为,链接器链接动态库时,默认的搜索路径是/usr/lib,而此时在/usr/lib目录下找不到我们自定义的libvector.so。因此,要先把libvector.so复制到/usr/lib下(注:要在root用户下)
kkmjy@ubuntu:staticLib$ sudo cp libvector.so /usr/lib
然后再执行p就没有问题啦。
以上是具体的过程。而理论部分,我们需要了解的是——可执行目标文件p在运行时可以和libvector.so链接,而这个时候,其实没有任何libvector.so的代码和数据真的被拷贝到可执行文件p中。反之,链接器拷贝了一些重定位和符号表信息,它们使得在运行时可以解析对libvector.so中代码和数据的引用。
下图概括了动态链接的过程。