静态链接-空间与地址分配

当我们有两个目标文件时,如何将它链接起来成一个可执行文件?这个过程发生了什么?这基本上就是链接的核心内容:静态链接。 我们以使用下面源文件“a.c”和“b.c"作为例子展开分析:

/*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.o和b.o;从代码中可以看到,“b.c”总共定义了两个全局符号,一个是变量“shared”,另外一个是“swap”;“a.c”里面定义了全局符号是“main”。模块a.c引用到了b.c中的swap和shared。接下来我们要做的就是将“a.o”“b.o”这两个目标文件链接在一起并最终形成一个可执行文件"ab";

1.空间与地址分配

对于链接器来说,整个链接过程中,它就是将几个输入目标文件加工后合并成一个输出文件。可执行文件中代码段和数据段都是由输入的目标文件中合并而来的,那么我们链接过程就很明显产生了第一个问题,对于多个输入目标文件,链接器如何将它们的各个段合并到输出文件?或者说,输出文件中的空间如何分配给输入文件?

1.1 按序叠加

一个最简单的方案就是将输入目标文件按照次序叠加起来,如图1所示:

问题: 这样在有很多输入文件的情况下,输出文件将会有很多零散的段。比如一个稍大规模的应用程序可能会有数百个目标文件,如果每个目标文件都分别有.text段、.data段和.bss段,那么最后输出的文件会有许多零散的段。这种做法非常浪费空间,因为每个段都会要求字节对齐要求,比如对于x86空间来说,段的装载地址和空间的对齐单元为页,也就是4096字节。那么就是说如果一个段的长度只有一个字节,它也要在内存中占用4096字节,这样会造成空间内存的大量内部碎片。所以并不是很好的方案;

1.2 相似段合并

一个更实际的方法是将相同性质的段合并在一起,比如讲所有输入文件“.text”合并到输出文件的“.text”段,接着“.data”段、“.bss”段等,如图4-2所示:

正如我们前文所提到的,“.bss”段在目标文件和可执行文件中并不占用文件的空间,但是它在装载时占用地址空间。所以在链接器在合并各个段的同时,也将“.bss”合并,并且分配虚拟空间。 “链接器为目标文件分配地址和空间”这句话中的“地址和空间”其实有两个含义:

  1. 在输出的可执行文件中的空间;
  2. 装载后的虚拟地址中的虚拟地址空间。

比如在“.text”和".data"来说,它们在文件中和虚拟地址都要分配空间,因为它们在这两者都存在;而在“.bss”这样的段来说,分配空间只局限与虚拟地址空间,因为它在文件中并没有内容。==事实上,我们在这里谈到的空间分配只关注于虚拟地址空间分配;==

现在的链接器空间分配策略基本上采用上述方式中的第二种,使用这种方法的链接器一般都采用一种叫两步链接的方法。也就是整个链接过程分两步。

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

我们使用ld链接器将“a.o”和“b.o”链接起来:

$ ld a.o b.o -e main -o ab
  • -e main 表示将main函数作为程序入口,ld链接器默认入口为_start。
  • -o ab表示链接输出文件名为ab,默认为a.out。 让我们使用objdump来查看链接前后地址分配情况,代码如清单4-1所示:

VMA表示虚拟地址,LMA表示加载地址,正常情况下两个值都是一样的,但是在有些嵌入式系统中,特别是程序放在ROM的系统中时,LMA和VMA是不相同。这里我们只要关注VMA即可。 链接前后的程序所使用的地址已经是程序在进程中的虚拟地址,即我们关心上面的VMA和Size,而忽略文件偏移。我们可以看到,在链接之前,目标文件中的所有段VMA都是0,因为虚拟地址还没有分配,所以它们默认都为0;等到链接的之后,可执行文件“ab”中的各个段都被分配到了相应的虚拟地址。

1.3 符号地址的确定

我们还是以“a.o”和“b.o”作为例子,来分析两个步骤中链接器的工作过程。在第一步的扫描和空间分配阶段,链接器按照前面介绍的空间分配方法进行分配,这时候输入文件中的各个段在链接后虚拟地址就已经确定,比如“.text”段起始地址为0x08048094,“.data”段的起始地址位0x08049108; 当前面一步完成之后,链接器开始计算每个符号的虚拟地址,因为每个符号在段内的相对位置是固定的,所以其实“main”、“shared”和“swap”的地址已经是确定的了,只不过链接器需要给每个符号增加上一个偏移量,使它们能够调整到正确的虚拟地址。 比如我们假设“a.o”中的main函数相对于“a.o”的text段的偏移是X,但是经过链接合并后,“a.o”的“text”段位于虚拟地址的0x08048094,那么“main”的地址段位于虚拟地址的0x08048094+x,从前面的objdump输出可以看到,“main”这个符号在最终输出文件中的地址应该是0x08048094+0,即0x08048094。我们也可以通过相同方式知道符号的地址;

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏企鹅号快讯

Jmeter常用获取数据的几种方式

还没用过聊天机器人?给我发消息试试。 Jmeter在互联网测试中应用非常多,可以用来做接口测试或者性能测试,算是非常不错的一个工具。今天我们来聊聊Jmeter获...

2198
来自专栏Java编程技术

深入浅出一致性Hash原理

在解决分布式系统中负载均衡的问题时候可以使用Hash算法让固定的一部分请求落到同一台服务器上,这样每台服务器固定处理一部分请求(并维护这些请求的信息),起到负载...

821
来自专栏烙馅饼喽的技术分享

用ECMAScript4 ( ActionScript3) 实现Unity的热更新 -- 使用第三方组件

Unity开发中,常常会用到一些第三方组件。本文以实例介绍如何在热更新脚本中使用这些第三方组件。 首先说明几个基本步骤: 第三方组件通常是以dll或者源码方式提...

38510
来自专栏安恒网络空间安全讲武堂

CVE-2011-0104分析

关于office的漏洞。先把进程attach上去然后打开exploit的文件。查看程序崩溃在这里:

1393
来自专栏深度学习之tensorflow实战篇

RStudio中,出现中文乱码问题的解决方案

RStudio中,出现中文乱码问题的解决方案 正常出现乱码问题, 解决步骤: 1、设置RStudio文本显示的默认编码: RStudio菜单栏的Tools...

2807
来自专栏用户2442861的专栏

linux grep工作常用

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/haluoluo211/article/d...

1184
来自专栏PHP实战技术

Linux下自动化监控内存、存储空间!

距离上一次更新文章已经过去一段时间了,小编在这段时间因为一些琐事,加上身体生病不能及时更新文章,今天身体逐渐恢复就急忙来更新文章,今天思梦给大家...

3486
来自专栏日常分享

RabbitMQ基本模式

   最近用到了一些RabbitMQ的东西,看了官方的Get Started,以此为模板总结一下。

2992
来自专栏H2Cloud

C++ 多线程编程总结

C++ 多线程编程总结          在开发C++程序时,一般在吞吐量、并发、实时性上有较高的要求。设计C++程序时,总结起来可以从如下几点提高效率: l ...

3716
来自专栏gaoqin31

设计模式之 策略模式

定义 :封装了一些列算法,它们之前可以相互替换,此模式使得算法的改变,不会影响到使用它们的客户端

1283

扫码关注云+社区

领取腾讯云代金券