本文主要内容是我在《CSAPP》一书中关于链接一章的笔记。链接作为一个高级语言编写的代码变为可执行文件的步骤之一,在编译时/加载时/运行时起到了非常重要的作用。
接下来是本篇博客的主体目录:
接下来看第一部分:链接是什么?
《CSAPP》一书中是这样定义链接的:
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程。该文件最终会被运行。
可知,链接是起到了合并多个部分组成一个完整可运行程序的功能。然而我们都知道链接在一个高级语言代码变成可执行文件时起的效果是如下图所示的:
可知,在hello.c的处理过程中,链接参与了最后一步,将printf.o与hello.o链接起来成为一个完整的可执行文件。因此,链接的作用在于,用户们在编写大型项目时,不用将整个文件放在一个巨大的源代码内,而是可以将其分为更小的模块,当我们需要修改它时,只需要对其需要修改的特定部分操作,并在修改后重新进行编译和链接即可。
了解完是什么的问题,我们来看“为什么”,为什么我们需要进行链接这一部分呢?
正如前一个部分里所说的,一个大型代码在链接的帮助下,可以被模块化为多个源文件,修改起来也更加容易。并且,当一些代码复用性非常高时,我们就可以对它建立公共函数库(也就是一个代码包),在使用到这些代码时我们只需要链接到它所在的公共函数库即可。
这里的效率我们从时间、空间两个维度来说;时间上,因为链接的模块化,我们在对一个文件进行编译时,可以对不同的模块分别编译,现在没有用到的模块,就不编译;现在用到的模块,优先编译,这样子便提高了代码的编译效率。空间上,类似的代码可以被写在公共函数库内,而不是每个使用到这段代码的地方都写入这一段代码,从而节省了空间。
链接主要是通过链接器完成的,而链接器主要要在链接过程中完成两项任务:1.符号解析:目标文件定义并且引用符号,而每个符号对应一个函数/全局变量/静态变量。而符号解析就是让上述符号和它对应的部分联系起来;2.重定位:链接器通过把每个符号定义与一个内存位置对应起来,从而重定位那些节,然后使用汇编器产生的这些符号的引用,使上述符号指向之前的内存位置。
而目标文件具有以下三种类型:1.可重定位目标文件(.c);2.可执行目标文件(a.out);3.共享目标文件(.so)。
gcc -c srcfile filename.c
gcc -o srcfile filename.c
现代的Linux系统使用ELF(可移植可执行文件格式)来描述可重定位目标文件的,其结构如下图所示:
以下是一些重要的节的解释:
其中我们可以看到一个节叫做.symtab,里面存放着各类本段代码中定义和引用的符号。那么此符号非彼char类型符号;在链接的上下文中,一共有着三种符号。
了解完可重定位文件的定义以及符号的组成,不禁发问:链接器是如何处理这些符号的?这里就要提到符号解析了:
链接器解析符号引用的方法是将每个引用与之前提到的可重定位目标文件的ELF表中的.symtab联系起来。这里给出一个判别符号强弱等级的定义:
我们都知道,变量声明与函数定义时,不同的模块之间是非常容易发生重名的,无论是变量名与函数之间,还是强符号与弱符号之间,那么链接器在符号解析的时候该如何处理重名的情况呢?Rules如下:
上有政策,下有对策,作为程序编写者的我们,应当如何应对链接器给出的规则?Strategies如下:
接下来我将运用代码来解释如何从建立一个静态库到调用一个静态库。
静态库vector.h中的代码之一addvec.c:
/*addvec.c*/
int addcnt=0;
void addvec(int *x,int *y,int *z,int n){
int i;
addcnt++;
for(i=0;i<n;i++)
z[i]=x[i]+y[i];
}
静态库vector.h中的代码之二multvec.c:
/*multvec.c*/
int multcnt=0;
void multvec(int *x,int *y,int *z,int n){
int i;
multcnt++;
for(i=0;i<n;i++)
z[i]=x[i]*y[i];
}
我们如何将这一段代码引入到我们自己建立的静态库vector.h中呢?这时就需要用到AR工具,命令如下:
linux> gcc -c addvec.c multvec.c
linux> gcc -c ar rcs libvector.a addvec.o multvec.o
第一条命令意在将addvec.c和multvec.c变为可重定位目标文件,第二条命令意在创建一个包含了之前两个.o文件的静态库。
为了使用这个库,我们可以再编写一个应用程序:
/*main.c*/
include <stdio.h>
include "vector.h"
int x[2]={1,2};
int y[2]={3,4};
int z[2];
int main{
addvec(x,y,z,2);
printf("z=[%d,%d]",z[0],z[1]);
return 0;
}
为了让上面的代码能够真的实现,我们要编译和链接main.o和之前创建的静态库libvector.a,命令如下:
linux> gcc -c main.c
linux> gcc -static -o prog main.o ./libvector.a
第一行命令意在将main.c转化为可重定位目标文件;
第二行命令:prog为可执行文件名,"./"意思是shell运行符,架在libvector.a之前说明命令要求shell运行libvector静态库。这里就可知,一旦调用了某个库中的代码,则一定要将被调用库的名字写在调用者的后面,且静态库一定要在.o文件之前。
综上便是一个完整的创建静态库、调用静态库的过程。
接下来是一些关于调用静态库时命令行操作的习题,习题来自于《CSAPP》第七章课后习题:
a,b代表当前目录中的目标模块或者静态库,而a->b表示a依赖于b。对于下列场景,请给出最小的命令行,使得静态链接器能够正常解析所有的符号引用:
A: p.o->libx.a->liby.a
B: p.o->libx.a->liby.a且liby.a->libx.a->p.o
对于A情况,我们可以很容易地写出:
linux> gcc -static prog p.o libx.a liby.a
对于B情况,可知p依赖于x,x依赖于y;而y又依赖于x,x又依赖于p;又知静态库名一定要在.o文件之前的原则,可以写出:
linux> gcc prog p.o libx.a liby.a libx.a
就是正确答案。
本篇博客中主要以“是什么->为什么->怎么办->怎么完成更高级的功能”的思路探讨了链接及链接器的知识,希望对大家有所帮助。