三、汇编
编译过程就是生成汇编代码的过程,在编译过程中,也会调用汇编器 as,将源代码生成汇编代码。比如,执行 gcc -S hello.c -o hello.s
此时已经生成了汇编代码。
汇编的过程就是将 hello.s 生成目标文件。
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。汇编器的汇编过程相对于编译器来讲比较简单,只是根据汇编指令和机器指令的对照表一一翻译就可以了。它没有复杂的语法,也没有语义,也不需要做指令优化。
汇编过程可以调用汇编器 as 来完成:
as hello.s -o hello.o 或者 gcc -c hello.s -o hello.o
也可以使用 gcc 命令从 C 源代码文件开始编译,经过预编译,编译和汇编直接输出目标文件(Object File):
gcc -c hello.c -o hello.o
四、链接
链接的过程就是生成 a.out 的过程。此过程中分为静态链接和动态链接两种。
4.1 静态链接
可以先看看一个静态链接的过程:
ld -static /usr/lib/crtl.o /usr/lib/crti.o -L/usr/lib/gcc/xxxx/xxx -L/usr/lib -L/usr/lib -L/lib hello.o --start-group -lgcc -lgcc_eh -lc --end-group /usr/lib/gcc/xxxx/xxx /usr/lib/crt0.o
链接的过程就是一个模块拼装的过程,将各种模块通过符号拼装成一个整体,即 a.out 。链接的主要内容就是把各个模块之间相互引用的部分都处理好,使得各个模块之间能正确的衔接。
链接过程主要包括了地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定位(Relocation)等这些步骤。
静态链接过程图如下:
源码文件(.c)文件经过编译器编译成目标文件(Object file,一般扩展名为 .o 或 .obj),目标文件和库(Library)一起链接形成最终的可执行文件。
静态链接过程:
我们在程序模块 main.c 中使用另外一个模块 fun.c 中的函数 foo()。在 main.c 模块中每一处调用 foo() 的时候都必须确切知道foo() 函数的地址,但是每个模块都是单独编译的,在编译器编译 main.c 的时候它并不知道 foo() 的地址,所以它暂时把这些调用 foo() 的指令的目标地址搁置,等待最后链接的时候由链接器去将这些指令的目标地址修正。如果没有链接器,需要我们手工把每个调用 foo() 的指令修正,填入正确的 foo 函数地址。
当 func.c 模块被重新编译的时候,foo() 函数的地址有可能改变的时,那么在 main.c 中所有使用到 foo 的地址的指令将要全部重新调整。这个工作将是繁琐的,但是使用链接器,我们可以直接引用其他模块的函数和全局变量而无需知道它们的地址。因为链接器在链接的时候,会根据我们所引用的符号 foo ,自动去相应的 func.c 模块查找 foo 的地址,然后将 main.c 模块中所有引用到 foo 的指令重新修正,让它们的目标地址为真正的 foo 函数的地址。
上面就是静态链接最基本的过程和作用。
地址修正的过程也被叫做重定位(Relocation),每个要被修正的地方叫一个重定位入口(Relocation Entry)。重定位所作的就是给程序中每个符号的绝对地址引用的位置'打补丁',是他们指向正确的地址。
通过一个例子来具体分析静态链接的过程:
建立如下两个文件
编译: gcc -c a.c b.c ,生成 a.o b.o 的目标文件。
b.c 中有两个全局符号:shared 和 swap;a.c 中定义了全局符号 main。a.c 中引用了 b.c 中的两个全局符号。接下来要将 a.o 和 b.o 两个文件链接在一起形成一个可执行文件'ab'。
4.1.1 地址空间分配
对于链接器来说,整个链接过程中,就是将几个输入目标文件加工后合并成一个输出文件。对于多个输入目标文件,链接器通过下面的方法将它们的各个段合并的输出文件(即输出文件中的空间地址分配给输入文件)。
1.按序叠加
按序叠加即是将输入的目标文件按照次序叠加起来。如下图:
这样做会造成在有很多输入文件的情况下,输出文件将会有很多零散的段。一般不 采用这种方式
2.相似段合并
相似段合并就是将相同性质的段合并到一起,如下图所示:
.bss 段在目标文件和可执行文件中并不占用文件的空间,但是它在装载时占用地址空间。所以链接器在合并各个段的同时,也将'.bss'合并,并且分配虚拟空间。
'链接器为目标文件分配地址和空间'中地址和空间有两个含义:
第一个是在输出的可执行文件中的空间
第二个是在装载后的虚拟地址中的虚拟地址空间
对于实际数据的段,如 .text 、 .data,它们在文件中和虚拟地址中都要分配空间,因为这两者中都存在
对于 .bss 这样的段,分配空间的意义只局限于虚拟地址空间,因为它在文件中没有内容
我们只关注虚拟地址的分配。
现代链接器空间分配的策略都采用第二种方法进行分配,使用这种方法的链接器采用两步链接(Two-pass Linking):
第一步,空间和地址分配
扫描所有的输入目标文件,并且获得它们各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表。这一步中,链接器将能够获得所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度和位置,并建立映射关系
第二步,符号解析与重定位
使用第一步收集到的所有信息,读取输入文件中段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等。第二步是链接过程的核心,特别是重定位过程。
链接 a.o 和 b.o:gcc -o ab a.o b.o(不能直接用 ld 链接器,要使用的话也得指定当前使用得gcc 中得链接器工具)
链接完成后,使用 objdump 查看输出后得 ab 文件:
ab
链接后得程序中所使用得地址已经是程序在进程中的虚拟地址,即各个段的 VMA(Virtual Memory Address)和 Size,而忽略文件偏移(File off)。
链接前,目标文件中的所有段的 VMA 都是 0 ,因为虚拟地址空间还未分配,等到链接之后,可执行文件 'ab' 中的各个段都被分配到了相应的虚拟地址。这里的输出程序'ab'中,'.text'段被分配到了地址 0x00400450,大小为 212 个字节;'.data'段从地址 0x00601028 开始,大写为 14 个字节,整个链接过程前后,目标文件中各段的分配、程序虚拟地址如下图:
当前因为程序未指定程序入口为 main 因此合并后的 .text 和 .data 会大于a.o 和 b.o 之和。可以用反汇编指令查看 ab ,看看程序从哪里开始的。