静态链接

静态链接

命令:

objdump:

  • -r:查看目标文件的重定位表

ld

  • -e:-e main将main函数作为程序入口,ld链接器默认的程序入口为_start
  • -s:在默认情况下,ld链接器在生成可执行文件时会产生三个段(段名字符串表、符号表和字符串表),对于可执行文件来说,符号表和字符串表是可选的,但是段名字符串表是必不可少的-s参数禁止链接器产生符号表,或者使用strip命令来去掉程序中的符号表。

一.空间与地址分配:

多个目标文件链接过程是将相似段合并,一般采用两步链接的方法:

  1. 第一步空间与地址分配:扫描所有的输入目标文件,并且获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表。这一步中,链接器将能够获取所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并的长度与位置,并建立映射关系;
  2. 第二步符号解析与重定位:使用上面第一步中收集到的所有信息,读取输入文件中段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等。

二.重定位表:

重定位表用来保存这些与重定位相关的信息,在ELF文件中往往是一个或多个段。

对于可重定位的ELF文件来说,它必须包含有重定位表,用来描述如何修改相应的段里的内容。对于每个要被重定位的ELF段都有一个对应的重定位表,其实重定位表就是重定位段。可以通过objdump -r xx命令来查看目标文件中要重定位的符号。

当链接器需要对某个符号的引用进行重定位时,它就要确定这个符号的目标地址。这时候链接器就会去查找由所有输入目标文件的符号表组成的全局符号表(在第一步就会生成),找到相应的符号后进行重定位。

三.COMMON块:

关于COMMON块,需要首先讲一下BSS段:

BSS段通常指用于存储未初始化或初始化为0的全局变量和局部静态变量。在目标文件中,由于未初始化的全局变量不会在BSS段分配空间,而是标记其为COMMON块,因此通过readelf -S查看到的BSS的属性Size大小就是当前目标文件中所有局部静态变量的总大小

那如何记录未初始化的全局变量的大小呢?在ELF文件中有一个符号表Symtab,其中记录了符号信息,包括未初始化全局变量名称在字符串表中的索引,地址,大小等。

从符号的角度解释未初始化全局变量在BSS段为什么不占空间:
由于未初始化的全局变量会被当做弱符号,而弱符号在生成目标文件的时候,并不知道该符号真正需要的空间大小,因为如果其他目标文件也存在一个名字相同的全局变量,那么如果这个变量是强符号,那么这个符号的大小由这个强符号的大小决定;如果这个变量是弱符号,但是所占空间大小比之前的弱符号大,那么之前的符号的空间大小也会由这个符号的空间大小决定。所以在链接之前,目标文件根本不知道这个弱符号到底需要多大的空间,因此不会为这个未初始化的全局变量分配空间,只是在符号表中记录当前目标文件中这个符号所占据的空间大小。当然,在链接完成之后,这些符号还是会在BSS段中分配空间,这里的空间就不再是ELF文件中的空间了,而是装载之后的虚拟空间。

GCC的“-fno-common”允许把所有未初始化的全局变量不以COMMON块的形式处理,或者使用“_attribute_(nocommon)”,一旦一个未初始化的全局变量不是以COMMON块的形式存在,那么它就相当于一个强符号。

四.C++相关问题:

4.1 重复代码消除:

c++编译器在很多时候会产生重复的代码,比如模板、外部内联函数和虚函数表在不同的编译单元里生成相同的代码。举个例子,由于当前目标文件不知道其他目标文件是否已经实例化了,导致在不同的模块可能存在多个实例,造成空间的浪费、地址容易出错、指令缓存命中率低问题。

解决的办法,将每个模板的实例代码都放在单独的一个段里面,如方法add()的一个实例add,放在.temp.add段里面,那么在链接过程中,将相同的段合并,将相同的代码舍去。

4.2 函数级别链接:

有时候一个目标文件中有很多函数,但是我们只需要使用其中一个或极少个函数,那么就会导致大部分的空间是浪费的。为了解决这个问题,有些编译器提供了一个编译选项叫“函数级别链接”,这个选项会让所有函数都单独存放在一个段里面,链接的时候,只将需要使用的函数所在的段合并。

GCC编译器提供了类似的机制,“-ffunction-sections”和“-fdata-sections”,这两个选项的作用就是将每个函数变量分别保存在单独的段中。

4.3 全局构造与析构:

我们知道C++的全局对象的构造函数在main之前执行,C++的全局对象的析构函数在main之后执行。

ELF有两个特殊的段:

  • .init:该段里面保存的是可执行指令,它构成了进程的初始化代码。当一个程序开始运行时,在main函数被调用之前,Glibc的初始化部分安排执行这个段中的代码
  • .fini:该段保存着进程终止代码指令,当一个程序的main函数正常退出时,Glibc会安排执行这个段中的代码

那么如果一个函数放到.init段,在main函数执行前系统就会执行它。同理,如果一个函数放到.fini段,在mainn函数返回后该函数就会被执行。利用这两个特性,C++的全局对象的构造和析构函数就是由此实现的

你可能感兴趣的:(链接,装载,库)