代码改变世界

对链接的认识

2013-08-21 19:47  zhanjl  阅读(302)  评论(0)    收藏  举报

之前对链接过程一直都很模糊,总感觉很神秘,这两天看了一些有关链接的资料,对链接过程有了一个大概的认识,写这篇博客的目的是想整理一下自己的思路,如果有不对的地方,欢迎指导。

 我们知道源文件经过预处理器、编译器、汇编器会生成一个可重定位目标文件,链接器的作用就是把多个可重定位目标文件以及一些必要的系统目标文件组合起来,创建一个可执行目标文件。

本文以下面两个源文件为例子,来说明链接的过程

/*main.c*/
void swap();
int buf[2] = {1,2};
int main()
{
     swap();
     return 0;
}

/*swap.c*/
extern int buf[];
int *bufp0 = &buf[0];
int *buf1;

void swap()
{
      int temp;
      bufp1 = & buf[1];
      temp = *bufp0;
      *bufp0 = *bufp1;
      *bufp1 = *bufp0;
}

编译链接过程如下:

  

首先,要明确一点,链接的主要任务是什么,链接器完成两个任务:符号解析和重定位。

符号解析:就是将每个符号引用和一个符号定义联系起来,这里所说的符号只包括函数和全局变量(外部变量),局部变量是由运行时栈管理的,不是链接器的任务。

重定位:为可重定位目标文件分配实际运行地址之后,修改所有对这些符号的引用。

比如上文中的main.c源文件,符号解析主要是确定main函数所调用的swap()函数是否在其他源文件中定义,如果没定义则提示链接错误。

我们知道对源文件编译时并不知道最终文件在内存中的运行地址,代码和数据都是从0开始。地址重定位就是要确定运行时的存储器地址,并且要修改代码中对符号的引用,以便他们指向正确的运行时地址。(~ o ~)~zZ,下面会有详细说明。。。

例如main函数,对swap的调用在main.o中可能是

6:e8 00 00 00 00//call 00 00 00 00 也就是call <swap>

地址重定位之后就变为了

80483ba:e8 c8 83 04 08//0x80483c8是最终swap函数在内存中的地址。

 

 

下面我们就以静态链接为例,来说明链接器如何完成符号解析和重定位的任务的。动态链接大部分过程和静态链接相似,后面会给出不同的部分。

 

符号解析:

在这之前,先简单说一下可重定位目标文件的结构。

简单来说可重定位目标文件结构是分段的,重要的部分有文本段(存储指令)、数据段(存储全局变量和static静态变量,局部变量不归链接器管)、符号表(符号解析时用的)和地址重定位表(重定位用的)。

进行符号解析的第一步,说白了就是要向编译器说明我这个源文件需要什么符号,能提供什么符号。这些信息都存储在符号表中。

例如main.c源文件能提供buf,main,需要swap;swap.c文件能提供bufp0,bufp1,swap,需要buf。

第二步就是把每个文件中需要的符号和其他文件能提供的符号联系起来。main.c中需要swap,swap.c中能提供swap,把他们联系起来,swap.c需要buf,main.c能提供buf,把他们联系起来。需要的符号和能提供 的符合也有可能在同一个文件中,过程是一样的。

在这一步我们就能看到很多链接时错误发生的原因了,假如一个源文件需要一个符号,而所有其他源文件都不能提供这个符号,会发生错误。

假如所有源文件中提供的符号有相同的,也会发生错误。

这是不需要静态库的情况。但是一般我们都需要调用库函数,例如scanf,printf,rand等等。此时就需要和静态库链接了,其实过程和上述差不多,只不过查找符号表时,把静态库的符号表加进来了。静态库可以说是一个目标文件集合。

我们现在可以详细看一下符号解析的具体步骤。

定义三个集合E,U,D。E就是可重定位目标文件的集合,将来把他合成可执行目标文件,U前面文件未解析符号集合,D前面的文件能提供的符号集合。

第一步,判断命令行上面的每个输入文件f,看它是目标文件还是静态库文件,如果是目标文件,直接放入E中,根据f的符号表修改集合U和D,继续读入下一个文件。

第二步,如果f是静态库文件,则链接器就尝试匹配U中的符号和由静态库文件的成员定义的符号。如果匹配,把该成员加入到E中,修改U和D,直到静态库结束。继续读入下一个文件。

第三步,当读入所有文件后发现U不为空,则提示错误,否则,进行下一步,重定位。

这有个问题要注意了,命令行中静态库出现的位置非常重要。假如main.c中调用了printf,而命令行中静态库出现在main.o之前,会有链接错误,在main.o之后就不会有错误,想想为什么??

 

OK,到此符号解析就完成了,不麻烦吧。。。一句话:就是把需要的符号和能提供的符号联系起来。

 

 

开始重定位了:O(∩_∩)O

重定位之后,几个可重定位目标文件就成了可执行目标文件,可执行目标文件也是由段组成的,结构和可重定位目标文件差不多,也有文本段,数据段。

所以,重定位的任务一就是把几个可重定位目标文件的文本段、数据段合成一个文本段、数据段。直接把每个段放到一块就OK了。

然后就是要分配地址了,我们知道,编译时编译器并不知道代码和数据最终运行时的地址,也不知道他用到的其他文件定义的符号在哪,所以每个源文件的指令和数据都是从0地址开始的。

例如main.c的指令可能是:

0: 55       push %ebp

1: 89 e5    mov %esp,%ebp

6:e8 00 00 00 00  call <swap>

等等,它并不知道实际swap函数放在哪,所以可以用00 00 00 00 先代替。

此时,我们要给main.c和swap.c中的指令和变量分配地址了。

分配后main.c的指令可能是:

80483b4: 55       push %ebp

80483b5: 89 e5    mov %esp,%ebp

80483ba:e8 00 00 00 00  call <swap>

等等。

swap.c也同样,具体指令就不写了,swap中定义的全局变量也分配了实际地址。

但是此时有个问题

main.c中调用swap函数的指令是e8 00 00 00 00,实际给swap函数分配的地址可能不是00 00 00 00,可能是80483c8,怎么修改呢?

这时就需要重定位表喽。

所以,编译器在编译时,无论何时遇到对未知符号的引用,都会生成一个可重定位条目,用来指导链接器来修改这个引用,例如main中对swap 的引用。

可重定位条目简单的说包括引用的名字和地址,例如main中swap的引用是

名字   地址偏移量(相对于文本段)

swap  7

为什么是7? e8 00 00 00 00的偏移量是6,所以00 00 00 00的偏移量就是7,

然后查找swap的实际地址,假设地址为80483c8,然后覆盖偏移为7的这个地方,此时指令变为 e8 c8 83 04 08 call <swap>。 ok,重定位完成。可以试着分析一下swap.c的重定位。

重定位条目就是记录要修改的地方,以及要修改成什么数据,然后用新数据覆盖,就完成了。

此时就生成可执行目标文件了,确定了对每个符号的引用,在指令中填入了正确的地址。可以加载到内存中运行了。。

OK,上述就是静态编译的全部过程。

 

 

先写到这吧,下一篇再写动态链接,可执行文件到内存的映射,以及加载。顺便理一下运行时栈。