《程序员的自我修养——链接、装载与库》静态链接(四)

静态链接

 链接的核心内容就是将多个目标文件合并为一个可执行文件

举两个例子:

/*a.c*/
extern int shared;
int main()
{
    int a = 100;
    swap(&a,&shared);
}

/*b.c*/
int shared = 1;
void swap(int* a,int* b)
{
    *a ^= *b ^= *a ^= *b;
}

两个文件分别定义不同的符号,链接可使得 a.c 引用到 b.c 的定义
$gcc -c a.c b.c

内存空间分配

 引出问题:如何将多个目标文件的各段合并到输出文件,其内存空间如何分配?

一、按序叠加
 直接将所以目标文件依次叠加合并。不可取,当应用程序规模较大时,比如可能有上百个目标文件,最后的输出文件则会有成百上千个零散段。x86 的段装载地址和空间的对齐单位是页,也就是 4096 字节,哪怕一个段只有一字节也要占用 4096 字节,会造成内存严重浪费。

二、相似段合并
 这是目前链接器基本都在正在使用的分配策略。
 将性质相同的段合并在一起,比如将所有文件的 .data 合并为输出文件的 .data 段,以此类推。.bss 段相较于其它段特殊一点,它不占用文件的存储空间,但是装载时占用地址空间。

  • 第一步 空间与地址分配:扫描所有输入文件,获取它们各段的长度、属性和位置等,并将输入文件的符号表中所有符号定义和符号引用收集,统一生成一个全局符号表。这一步将输入文件合并,并且计算输出文件各段的长度和位置,建立映射关系。

  • 第二步 符号解析与重定位:这一步是链接过程的核心,将上一步收集到的所有信息进行符号解析和重定位、调整代码中的地址等。

 上面说的都是输出文件装载入内存的地址空间分配问题,而关于输出文件存储空间的分配和链接过程的关系并不大,就不做多的讨论。

链接 a.o 和 b.o:
ld a.o b.o -e main -o ab

LMA :加载地址
VMA :虚拟地址

链接后程序中使用的地址已经是程序在进程中的虚拟地址

符号地址的确定

 在第一步扫描和空间分配的时候就已经确定了输入文件中各段在链接后的虚拟地址。在第一步段虚拟地址确定后,链接器开始计算各符号的虚拟地址。各符号在段内的偏移是固定的,所以函数名,变量等地址也就确定了,但仍然要给每个符号添加偏移量,使其能调整到正确的地址。

符号解析与重定位

重定位

首先了解的问题:在进行重定位之前,输入文件里的符号是怎样表示外部符号的?
 在编译时,所有的外部符号都采用 0x00000000 或者 0xfffffffc 来替代,真正的地址计算工作由链接器来完成。就是上面的第二步链接的工作之一,在完成地址和空间的分配后链接器根据每个需要重定位的符号地址进行修正。

重定位表

 上面链接的第一步中扫描所有输入文件时获取的所有符号信息都会被存储再生成的重定位表/段中。且按原符号所在的段进行区分,例如 .text 段存在重定位符号则相应存在 .rel.text,同理也能有 .rel.data。

 重定位表的组成是一个Elf32_Rel结构的数组,每一个数组元素对应一个重定位入口。

Elr32_Rel定义:

typedef struct {
    Elf32_Addr r_offset; //重定位入口的偏移,对于可重定位文件来说,这个值是改重定位入口所要修正的位置的第一个字节相对于段起始的偏移。对于与输出文件来说,又是该重定位入口要修正的位置的第一字节的虚拟地址。
    Elf32_Word r_info; //重定位入口类型或符号,低8位表示重定向入口类型,高24位表示改符号在重定向表中的下表。
}Elf32_Rel;

粗漏的描述就是,r_offset 是要重定位符号所在的位置(段起始的偏移),r_info 是符号类型

符号解析

 之所以要链接各个其它文件,是因为我们的目标文件中用到的符号被定义在其它文件里。
 当链接器需要对某个符号进行重定位时,它会去查找全局符号表以获取该符号的目标地址,然后进行重定位。如果没有进行链接,则全局符号表里没有该符号,报符号未定义错误。

指令修正方式

 根据符号类型不同即重定位类型不同,不同的重定位类型采用的重定位修正方法也不同。32位x86环境下ELF文件的修正指令寻址方式有两种。

  • 绝对地址修正
    宏定义:R_386_32

  • 相对地址修正:
    宏定义:R_386_PC32

 绝对寻址修正后的地址为该符号的实际地址,相对寻址修正后的地址为该符号距离被修正位置的偏移量。

具体计算方法看书P110

COMMON 块

COMMON 块概念

 COMMON 块是一种空间分配的机制,来源于早期的 Fortran 语言,当时没有动态分配空间机制,定义符号需要事先声明所需要的临时空间大小。Fortran 把这个空间称为 COMMON 块。当不同的目标文件需要的 COMMON 块大小不一致时,以最大的为准。

 再了解一下C语言中强符号和若符号,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号(C++并没有将未初始化的全局符号视为弱符号),强符号和弱符号的定义是连接器用来处理多重定义符号的。

 若符号机制允许同一个符号的定义存在于多个文件中,同时链接器是不支持符号类型的,即变量类型对于链接器来讲是忽略的,它只有一个符号名。这就产生了问题,要是同一个弱符号定义在多个目标文件中,而他们的类型又不同,该怎么办?

 首先强符号的定义是不允许出现在多个目标文件中的,会报符号重复定义错误。

那就存在另外两种情况:

  • 该符号在所有定义它的目标文件里都未定义,都作为弱符号。就按照 COMMON 类型的链接规则,最终在输出文件的大小以输入文件中最大的为准。

  • 该符号在一个文件里已经初始化,作为一个强符号,而在其它目标文件里未初始化,作为弱符号,就以作为强符号的定义为准,只是当有弱符号定义大小大于强符号定义时,ld链接器会报一个警告。

注意,这里都是讨论的未初始化的全局变量,而未初始化的局部静态变量不存在上述问题,后者是程序执行到定义该变量时临时开辟动态空间,之后也会将该空间释放。

静态链接库

链接库概念

 所谓库就是对操作系统的API包装,任何输入输出都是结果一系列处理之后调用系统提供的API的结果。静态库可以看成是一组目标文件的集合,即很多目标文件打包形成的一个文件。

比如:
Windows上的 .lib/.dll ,Linux上的 libc.a 等。

glibc 本身是用 C 语言开发的,由成百上千的源码文件组成,编译后也生成相同数量的目标文件,它包含输入输出 printf.o,scanf.o;文件操作有 fread.o,fwrite.o;时间日期有 date.o,time.o;内存管理有 malloc.o 等。

标准输入输出(stdin/stdout),以及标准错误输出(stderr)都定义在 stdio.o 里。

编译链接过程

 以 hello.c 为例

  • 第一步调用 cc1 程序,这就是 GCC 的C语言编译器,将 hello.c 文件编译为一个临时的汇编文件 hello.s 临时文件名称并不是这样的,这里只是方便表示后缀名,下面出最后的目标文件外也同此。
  • 第二步调用 as 程序,这是 GNU 的汇编器,将 hello.s 汇编为临时文件 hello.o 。
  • 最后一步调用 collect2 程序来完成链接,这个 coolect2 可视为 ld 链接器的一个包装,由它调用 ld 链接器。

链接控制

 在一些特殊情况,比如操作系统内核、BIOS、嵌入式系统程序或者没有操作系统的情况(引导程序等)进行编译时,会有很多的特殊条件,例如硬件要求,指定地址要求等。这就需要对链接过程进行控制。

常用的有三种控制方式:

  • 命令行控制,指定链接器的参数如 -o、-e 等
  • 将链接指令存放在目标文件里,编译器经常采用这种方法向链接器传递指令。
  • 使用链接控制脚本。

BFD库

 这是一个 GUN 项目,用于处理跨各种软硬件平台,通过统一的接口来处理不同的目标文件格式。BFD是binutils的子项目,BFD将目标文件抽象为一个统一的模型,在这个抽象的模型中也有描述整个目标文件属性的“文件头”,通过操作这个抽象的目标文件模型来实现操作所有的BFD支持的目标文件格式。

发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据