程序员的自我修养 第4章 静态链接

在这里插入图片描述
gcc -c a.c b.c
经过编译后,生成两个目标文件,a.o b.o

空间与地址分配

对于链接器,整个链接过程中,它就是将几个输入目标文件加工后合并成一个输出文件。
可执行文件中的代码段和数据段都是由输入的目标文件中合并而来的。
链接器是如何合并的?输出文件中的空间是如何分配的?

按序叠加
在这里插入图片描述
相似段合并
在这里插入图片描述
我们在这里谈论的空间分配只关注于虚拟地址空间的分配,因为这个关系到链接器后续的关于地址的计算步骤,而可执行文本本身的空间分配与链接过程关系并不是很大。

现在的链接器空间分配策略基本上都采用上述的第二种,即相似段合并的方法。使用这种方法的链接器都采用两步链接的方法。
第一步,空间与地址分配。扫描所有的输入目标文件,获取她们各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表中。这一步,链接器将能够获得所有输入目标文件的段长度,并且将他们合并,计算出输出文件中各个段合并后的长度与位置,并建立映射关系。
第二步,符号解析与重定位。使用上一步收集的所有信息,读取输入文件中段的数据、重定位信息,并且进行符号解析与重定位、调试代码中的地址等。

ld a.o b.o -e main -o ab
-e main 表示将main函数作为程序入口,ld链接器默认的程序入口为_start
在这里插入图片描述
其中VMA表示virtual memory address即虚拟地址
LMA表示load memory address,即加载地址

在链接之前,所有的VMA都是0,因为虚拟空间还没有分配。
链接之后,可执行文件ab中的各个段都被分配到了虚拟地址。VMA不为0.
在这里插入图片描述
但是为什么给ab文件的text段分配到0x08048094地址呢

符号地址的确定
当前面一步进行完成,链接器就开始计算各个符号的虚拟地址了。链接器会给每一个符号加上一个偏移量,使他们能够调整到正确的虚拟地址。

符号解析与重定位

重定位

在这里插入图片描述
编译器把这两条指令的地址部分暂时用地址0x00000000和0xFFFFFFFC来代替,把真正的地址计算工作交给了链接器。
在这里插入图片描述
重定位表
链接器怎么知道哪些指令是需要被调整的呢?这些指令哪些部分要被调整?怎么调整?这些都需要重定位表来提供信息。
如前一章所述,重定位表的功能就是专门用来保存这些与重定位相关的信息,用来指示哪些指令需要调整,以及怎么调整。
可以使objdump -r来查看目标文件的重定位表。
在这里插入图片描述
每一个要被重定位得到地方就叫一个重定位入口。
重定位入口的偏移offset表示该入口在要被重定位的段中的位置。
Relocation records for .text表示这个重定位表示代码段的重定位表。
这里的1c和27就是前面目标文件中需要重定位的位置。
在这里插入图片描述
符号解析
重定位的过程中,每个重定位的入口都是对一个符号的引用,那么当链接器需要对某个符号的引用进行重定位的时候,他就要确定这个符号的目标地址。这时候链接器就会去查找由所有输入目标文件符号表组成的全局符号表,找到相依的符号后进行重定位。

在这里插入图片描述
扫描完所有的输入目标文件之后牛,所有的这些未定义的符号就应该能在全局符号表中找到,否则链接器就会报符号未定义错误。

指令修正方式
对于32位x86平台ELF文件的重定位入口所修正的指令寻址方式只有如下两种:

  • 绝对近址32位寻址
  • 相对近址32位寻址

这两种重定位方式指令修正方式每个被修正的位置长度都是32位,即四个字节。
在这里插入图片描述
在这里插入图片描述
绝对寻址修正
main函数的虚拟地址为0x1000, swap函数的虚拟地址为0x2000, share变量的虚拟地址为0x3000
在这里插入图片描述
相对寻址修正
在这里插入图片描述

COMMON块

当多个符号定义类型不一致时,链接器如何处理?
多个符号定义类不一致的几种情况:

  • 两个或者两个以上强符号类型不一致
  • 有一个强符号,其他都是弱符号,出来类型不一致
  • 两个或者两个以上弱符号类型不一致

现在的编译器和链接器都支持COMMON块的机制。
现代的链接机制在处理弱符号时候,采用的就是与COMMON块一样的机制。编译器会把未初始化的全局变量定义作为弱符号。而未初始化的局部静态变量存储在bss段中。
比如global_uninit_var
在这里插入图片描述
这是一个全局的数据对象,类型为SHN_COMMON类型。
链接之后的输出文件的弱符号所占的空间以每一个输入文件中弱符号所占空间中最大的为准。

COMMON块的目的就是编译器和链接器允许不同类型的弱符号存在,但是本质原因还是链接器不支持符号类型,即链接器无法判断各个符号的类型是否一致。
当编译器讲一个编译单元编译成目标文件的时候,如果该编译单元包含了弱符号,那么该弱符号最终所占空间的大小在此时是未知的,因为有可能其他编译单元中该符号所占的恐案件比本编译单元该符号所占的空间哟啊大。所以编译器此时服务发该符号在BSS段中分配空间,因为所需要的空间大小未知。但是链接器在连接过程中可以确定弱符号的大小,因为当链接器读取所有输入目标文件以后,任何一个弱符号的最终大小都可以确定了,所以它可以在最终输出文件的BSS段为其分配空间。总体来看,未初始化的全局变量最终还是被存放在BSS段。
GCC的-fno-common允许把所有未初始化的全局变量不以COMMON块的姓氏处理。
int global __attribute__ ((nocommon));

C++ 相关问题

C++的一些语言特性必须由编译器和链接器共同支持才能完成工作。最主要的两个是,一是C++的重复代码消除,另一个是全局构造与析构。

重复代码消除
对于C++的模板、外部内联函数、虚函数表等有可能在不同的编译单元生成相同的代码。
一个比较有效的做法是讲每个模板的实例代码都单独地存放在一个段里,每个段只包含一个模板实例。比如有一个模板函数add(),某个编译单元以int类型和float类型实例化了一个模板函数,那么该编译单元的目标文件中就包含了这两个该模板实例的段。当别的编译单元也以int和float类型实例化该模板后,也会生成同样段名字的段,这样链接器在最终的链接的时候可以区分这些相同的模板实例段,然后将他们合并最后的代码段中。

GCC和Visual C++编译器都是采用了类似的做法。
gcc在最终合并的段名叫Link Once
Visual C++把这类叫做COMDAT,这个段都有IMAGE_SCN_LNK_COMDAT属性。

这些重复代码对于外部内联函数和虚函数的做法也是类似的。

函数级别链接
visual C++提供了一个编译选项叫函数级别链接,这个选项的作用就是让所有的函数像前面的模板一样,单独保存在一个段里面。当链接器需要用到某个函数时,它就将它合并到输出文件中,对于那些灭有用到的函数则将他们抛弃。这种做法很大程序上减小了输出文件的长度,减少了空间浪费。但是这个选项会减慢编译和链接的过程。

GCC提供了类似的选项 -ffunction-sections -fdata-sections, 这两个选项的作用就是将每个函数或者变量保存到独立的段中。

全局构造与析构
在main函数之前和程序运行结束需要一些工作要做。linux系统的一般程序的入口函数为_start
因为ELF定义了两个特殊的段才能完成这些工作。

  • .init 该段里面保存的是可执行指令,构成了进程的初始化代码
  • .fini 该段保存着进程终止代码指令。

C++和ABI
两个编译器编译出来的目标文件能够相互连接需要满足这些条件:采用同样的目标文件格式、拥有同样的符号修饰标准、变量的内存分布方式相同、函数的调用方式相同。这些规范统称为ABI(application binary interface)

影响ABI的因素很多,对于C语言的目标代码来说,有这几个方面

  • 内置类型的大小和在存储器中的放置方式
  • 组合类型的存储方式和内存方式
  • 外部符号与用户定义的符号之间的命名方式和解析方式
  • 函数调用方式
  • 堆栈的分布方式
  • 寄存器使用约定

对于C++来说,更加负责,需要考虑:

  • 继承体系的内存分布
  • 指向成员函数的指针的内存分布,如何通过指向成员函数的指针来调用成员函数,如果传递this指针
  • 如何调用虚函数,vtable的内容和分布形式,vtable指针在object中的位置等
  • template如何实例化
  • 外部符号的修饰
  • 全局对象的修饰
  • 全局对象的构造与析构
  • 异常的产生和捕获机制
  • 标准库的细节问题,RTTI如何实现
  • 内嵌函数访问细节

静态库链接

程序如何使用操作系统提供的API。在一般情况下,一种语言的开发环境往往会附带语言库。这些库就是对操作系统API的包装。比如printf,就是对write系统调用的封装。库里面还有一些很常用的函数。

一个静态库可以简单看成是一组目标文件的集合,即很多目标文件经过压缩打包后形成的一个文件。在linux最常用的C语言静态库libc位于/usr/lib/libc.a 数学与glibc项目的一部分。
下面是Windows下面的运行库
在这里插入图片描述
glibc由很多源文件组成的,编译完后的目标文件,经过ar压缩程序压缩到一起,并且对其编号索引,便于查找和检索,然后就形成了libc.a这个静态库。
在这里插入图片描述
visual C++也提供了类似的工具是lib.exe, 这个程序可以创建、提取、列举.lib文件的内容。

printf实例
在这里插入图片描述
gcc -c -fno-builtin hello.c
ar -x libc.a
ld hello.o printf.o
链接失败,因为系统库之间相互交织,相互调用。

在这里插入图片描述
在这里插入图片描述
其中cc1是编译器,as是汇编器,collect2程序是ld链接器的包装。

链接过程控制

我们一般使用链接器提供的默认链接规则对目标文件进行链接,不会存在什么问题。但是对于一些特殊要求的程序,就需要知道整个链接过程需要内容确定:使用那些文件、使用哪些库、输出格式等等

链接控制脚本
链接器一般提供三种方法:

  • 使用命令行来对链接器指定参数
  • 将链接指令存放在目标文件里面,编译器经常会通过这样的方式向链接器传递指令。
  • 使用链接控制脚本,这是最为灵活和强大的链接控制方式。

visual C++把这样的控制脚本叫做模板定义文件,扩展名为.def

linux下通过ld -verbose查看默认的链接脚本。存放在/usr/lib/ldscripts/下。
使用-T来指定一个链接脚本

最小程序
在这里插入图片描述
先使用下面方式以普通命令方式编译和链接
gcc -c -fno-builtin tinyhelloworld.c
ld -static -e nomain -o tinyhelloworld tinyhelloworld.o

使用ld链接脚本
在这里插入图片描述
命名为tinyhelloworld.lds
使用脚本链接
gcc -c -fno-builtin tinyhelloworld.c
ld -static -T tinyhelloworld.lds -o tinyhelloworld tinyhelloworld.o

ld链接脚本语法简介
在这里插入图片描述
在这里插入图片描述

BFD库

BDF(binary file description library)是希望通过一种统一的接口来处理不同的目标文件格式。
BFD是binutils项目的一个子项目。
现在gcc(gnu汇编器GAS GNU assembler)链接器ld 调试器GDB及binutils的其他工具都是通过BFD库来处理文件,而不是直接操作目标文件。这样做最大的好处是将编译器和链接器本身同具体的目标文件格式隔离开来。
BFD库在binutils-dev安装包中。现在BFD库已经支持大约25种处理器,将近50多种目标文件。
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sundaygeek

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值