C++开始前篇,深入编译链接(补充2)

  在开始链接之前,我们先了解几个概念:

一》符号的概念。

  我们知道,链接的最重要的是“对符号的重定位”,而且上面提到了符号表,那什么是符号呢,在链接中,我们将函数和变量统称为符号(Symbol)。函数名和变量名就是符号名(Symbol Name)。每一个目标文件都有一个相应的符号表(Symbol Table),这个表里记录了目标文件中所用到的所有符号。每个对应的符号有一个对应的值,叫做符号值(Symbol Value)。对于函数和变量来说,符号值就是它们的地址。     除了函数和变量,还存在其他几种符号。简单了解一下。     

    1,全局符号:定义在本目标文件中的全局符号     

    2,外部符号:在本目标文件中引用,却没定义在本目标文件,称为外部符号   

      3,段名:由编译器产生的符号

    4,局部符号:局部变量     

    5,行号信息 对于链接来说,其中最重要就是全局符号与外部符号。

二》关于ELF符号表的结构:

   a,这是段表

C++开始前篇,深入编译链接(补充2)

   b,这是符号表

C++开始前篇,深入编译链接(补充2)

我们可以找到两个表中以下的关系   

  1,从符号表的num下标为6开始看,static_init,是我们在定义过初始化了的局部静态变量,绑定属性是LOCAL,在段表中下标为3的段中存储,即在.data段;接下来是 static_uninit,是未初始化的局部静态变量,在段表中下标为4的段中存储,即.bss段。

2,再就是 global_init,与static_init相同,都保存在了.data段。而global_uninit,按照我们的分析,它跟staticy_uninit一样都应该保存与.bss段,这里却是COM,为什么?原来,这跟不同的语言与不同的编译器实现有关,有些编译器会将全局的未初始化变量存放在目标文件.bss段,有些则不存放,只是预留一个未定义的全局变量符号,等到最终链接成可执行文件的时候再在.bss段分配空间。(有关于此处的COM符号,在后面谈到关于COMMON块时再深入了解。)   

3,func函数与main函数都是全局可见的,因为是函数,所以类型是STT_FUNC。它们都存在与.text中。   

4,printf,因为我们只是使用却没有定义它,所以它在此目标文件中不存在于任何段,所以是UND(未定义)。

三》,强符号与弱符号。

  对于C/C++语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。

  针对强弱符号的概念,链接器会按如下规则处理和选择被定义多次的全局符号:

  规则1,不允许强符号被多次定义。否则报错。

  规则2,如果一个符号在某个目标文件中是强符号,在其他文件中都是弱符号,那么选择强符号。

  规则3,如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个。

-------------------------------------------------------------------------------------------------------------------------------------

  了解之后,现在是时候进入静态链接了。

  我们知道,链接是将所有目标文件以及库文件按照某种方式合并起来,最后生成可执行文件。是这么简单吗?是或不是,我们都要好好深入一下。

  我们写两个文件a.c和b.c

a.c

C++开始前篇,深入编译链接(补充2)

extern表明shared可在其他的文件中找。

b.c

C++开始前篇,深入编译链接(补充2)

编译之后得到a.o,b.o。链接之后得到ab

分别查看这三个文件的信息

C++开始前篇,深入编译链接(补充2)

C++开始前篇,深入编译链接(补充2)

C++开始前篇,深入编译链接(补充2)

  。先看ab的SIZE,稍微算一下我们可以发现,.text段和.data段它们的SIZE关系都是ab(SIZE)=a.o(SIZE)+b.o(SIZE);因此可以得出这是按相似段合并的。

。再看VMA,这是Virtual Memory Address,即虚拟地址。LMA表示Load Memory Addre1ss,即加载地址.除了在某些嵌入式系统不同,一般两个值都相同。a.o 和b.o中VMA和LMA都是0,因为编译阶段并不分配内存地址。

  。。这种合并方式一般采用一种叫做两步链接的方法,

  。。。第一步,空间与地址分配。

          问题:分配的是什么空间?             

此处的空间有两个含义,一是输出在可执行文件中的空间,二是在装载后的虚拟地址中的虚拟地址空间。我们可以看到,ab文件中并没有.bss段,原来对于.bss这样的段来说,分配空间的意义只局限于虚拟地址空间。在我们着重考虑虚拟地址空间。     

  总结一下第一步都干了些啥事:

链接之前,目标文件中所有段的VMA都是0,链接时,链接器按照相似段合并的方法在一个整合了两个目标文件的.text和.data段,并且给这些段分配了虚拟空间,也就有了虚拟地址。有了段的地址之后,链接器便开始计算每个段中的符号地址,因各符号在各段中的相对位置都是固定的,因此根据偏移量,可以得到各个符号的正确的虚拟地址。

  举个栗子:a.o中.text的虚拟地址是0x80488094,在.text段中main的偏移为X,则main的虚拟地址为0x80488094+X。

    。。。第二步,符号解析与重定位。

  完成了第一步空间与地址的分配后,链接器根据第一步收集到的信息,读取输入文件中段的数据,重定位信息并且进行符号解析和重定位、调整代码中的地址等。

OK,我们详细的来研究一下。

看一下a.o的反汇编代码:

C++开始前篇,深入编译链接(补充2)

程序的代码里面使用的都是虚拟地址,从上面反汇编结果可以看出 在目标文件a.o中,main的起始地址是0,因为还没有分配空间。等到空间分配完成后,各个函数才会确定自己在虚拟地址空间中的位置;

C++开始前篇,深入编译链接(补充2)

C++开始前篇,深入编译链接(补充2)

仔细观察这两行 ,,第11行  “c7 44 24 04”   是movl指令码, “00 00 00 00”这是shared的地址。

第20行  “E8”  是相对偏移调用指令call的指令码,“FC FF FF FF”是目的地址相对于下一条指令的偏移。

       当源代码a.c被编译成目标文件时,编译器并不知道shared和swap的地址,所以编译器暂时把0当做shared的地址;关于FC FF FF FF 是被调用函数的相对于调用指令的下一条指令的偏移量,这也只是一个临时的假地址,是常量-4的补码形式。

现在我们再看一看链接之后的结果:

C++开始前篇,深入编译链接(补充2)

我们发现,从链接结果来看,

原11行

C++开始前篇,深入编译链接(补充2)

原20行

C++开始前篇,深入编译链接(补充2)

它们的地址都被重写了。原20行,call指令的下一条是leave 它的地址是 80480b9   所以相对于leave指令偏移量为0x00000003 的地址为8048b9+3=8048bc。刚好是swap的地址!

  说到这儿,其实重定位的过程就是个符号解析的过程,在链接器扫描完所有的输入目标文件之后,所有这些未定义的符号都应该能在全局符号表中找到,否则链接器就会报符号未定义错误。(symbol UND).

  OK ,就到这儿,下一节,再来学习上次说到的COMMON块、静态库链接相关的知识。

上一篇:flashdevelop生成swc库


下一篇:SQL基础查询实战