前言
空间与地址分配描述了链接器如何将多个输入的目标文件合并到一个文件中,同时根据对应的运行平台,为可执行文件中的指令和符号分配运行时地址。
空间与地址分配
现代链接器执行的合并策略比较简单,通过将所有相同类型的节合并到一起,例如将所有输入目标文件的.text
节合并到输出文件的.text
节中;然后,链接器根据运行平台中进程虚拟地址空间的划分规则,为所有输入目标文件中定义的节和符号分配运行时内存地址;完成之后,程序中的每条指令和符号都有唯一的运行时内存地址了。链接器的空间分配示意如下:
在链接阶段,链接器会为会为所有的目标文件分配地址空间。这里的地址空间需要区分两种含义:
- 可执行文件自身的空间:用于磁盘上静态存储可执行文件的内容;
- 进程虚拟地址空间:由程序运行时,系统加载可执行文件的内容而动态建立。
**链接器为可执行文件中符号确定的地址即是最后程序运行时所使用的地址,在可执行文件被装载时会被一一映射到进程的虚拟地址空间。**典型的装载数据包括代码段和数据段中的数据,一些特殊的段,如.bss
段在可执行文件中不占用空间,但是在可执行文件装载后的进程虚拟地址空间中需要进行空间分配。对于其它一些如符号表、调试信息等数据,链接器不会为其分配地址空间,因为它们并不是程序运行所必要的数据,因此也不会被装载入进程虚拟地址空间中去。
空间与地址分配示例
为了说明空间与地址分配的过程,这里使用如下两个源代码文件进行分析,后续也会继续使用这两个文件:
首先使用gcc将main.c
和sum.c
编译成目标文件:
经过编译之后,已经生成了两个目标文件main.o
和sum.o
,在进行链接之前,先看下目标文件中的地址分配情况:
可以看到,生成的每个目标文件都有自己独立的代码段和数据段,并且所有的段都未分配虚拟地址空间,因此文件中所有段的虚拟内存地址(VMA字段)都是0。如果反汇编目标文件,可以看到每个目标文件的起始地址都是从0开始。
虚拟地址分配
现在我们使用ld链接器将main.o
和sum.o
链接起来生成可执行文件:
其中:
-e
选项用于指定main函数作为程序入口,ld链接器默认的程序入口为_start,定义于C运行库中,在此不链接使用C库的函数,因此需要单独指定程序入口。
链接后默认生成的可执行文件名为a.out
,可以查看a.out
的地址分配情况:
可以看到,在可执行文件中,链接器已经将所有输入目标文件的段进行合并,生成了新的段,并且都分配到了相应的虚拟地址。**当段的起始地址明确之后,段内的指令和数据地址也就逐一确定了。**这里虚拟地址的分配规则对于不同系统的实现是不同的,在32位Linux下,ELF可执行文件默认从地址0x8048000开始;对于64位Linux系统,则默认从0x400000开始。
符号地址
对于定义的全局符号,由于它们在段内的相对位置都是固定的,链接器只要加上相应的偏移,就可以依次计算出所有的符号的虚拟地址了。对于上面的代码中定义的两个全局符号array
和sum
,以外部函数sum
为例,计算出的虚拟地址值如下:
相关参考
- 《程序员的自我修养——编译、装载与库》
- 《深入理解计算机系统》