想一下, 我们想把源文件放到内存中执行,应该怎么做?
直觉上我们需要将源代码翻译成机器语言,以某种结构组织代码和数据。再让CPU去按这种结构读取指令。如果是多个源文件, 我们可能还需要按某种方式将它们组合到一块。 编译运行的原理其实大致类似,下面让我们看下具体流程:
一、 源文件的编译执行流程
链接(linking)是将各种代码和数据片段收集并组合成一个单一文件的过程, 这个文件可被加载到内存并执行。
首先, 来看两段C代码:
code/link/main.c
int sum(int *a, int n);
int array[2] = {1,2};
int main () {
int val = sum(array , 2);
return val;
}
code/link/sum.c
int sum (int *a, int n) {
int i, s = 0;
for(i = 0; i < n; i++){
s += a[i];
}
return s;
}
在linux 上我们可以通过系统提供的gcc编译器将其编译成可执行文件prog,命令如下:
gcc -o prog main.c sum.c
那执行这个命令时, 系统具体都做了哪些内容呢?如图所示展示了源文件编译链接的整个过程:
- 首先预处理将C的源文件main.c 翻译成一个ASCII 码的中间 main.i ,这个过程等价于命令:cpp main.c /tmp/main.i
- 编译器 ccl 将main.i 翻译成一个ASCII 汇编语言文件 main.s ,等价于: ccl /tmp/main.i -Og -o /tmp/main.s
- 然后,汇编器as , 将main.s 翻译成一个可重定位目标文件main.o, 等价于:as -o /tmp/main.o /tmp/main.s
- sum 的编译过程和main类似
- 链接器把main.o 和 sum.o 以及一些必要的目标文件链接在一起, 生成一个可执行目标文件。
- 最后, 我们用shell 命令./prog 执行程序。 操作系统会调用一个叫做加载器的函数(loader), 将可执行文件中的代码和数据复制到内存,然后将控制转移到程序开头。
二、可重定位目标文件的结构
在上面的过程中我们提到了两个目标文件: 可重定位目标文件和可执行目标文件。
目标文件, 其实就是子节序列以文件的形势存放在硬盘中。 源代码以某种格式序列化, 在运行的时候系统按找这种格式去找到每个函数,每个执行和每个变量的地址。 linux 以可执行可链接(ELF)格式存储。
为了更好的被CPU 识别, 想一下如何组织这些二进制代码可以更高效呢?
就像书桌上的物品要分类放置才整洁一样,为了便于管理翻译出来的二进制代码也分类存放,把表示代码的放在一起,表示数据的放在一起。这样,二进制代码就分为了不同的块来存放。这样的一个区域就是被称为段(segment)的东西。
首先,让我们来看一下由源文件编译产生的可重定位目标文件长什么样子:
- ELF 头: 记录程序入口地址 , 目标机器型号,版本号等信息。
- .txt : 文本段
- .data .bss : 数据段(.data 存放已初始化的数据, .bss存放未初始化的数据)
- .rel.text : 文本重定位段
- .rel.data: 数据重定位段(被模块引用或定义的全局变量的重定位信息)
- .symtab: 符号表 (符号表是一个符号信息的数组, 数组的每个条目中包含被分配到哪个节、 距该节起始位置的偏移)。
一般来说,代码中都会存在引用了外部的函数,或者变量的情况。既然是引用,那么这些函数、变量并没存在该目标文件内。在使用他们的时候, 就要给出他们的实际地址(这个过程发生在链接的时候)。而重定位表,提供了寻找这些实际地址的信息。 当汇编器遇到最终位置未知的目标引用时, 就会生成一个重定位条目。
一个可执行文件往往包含多个源文件? 在我们产生了多个可重定位目标文件之后, 链接器又是怎么把他们汇集到一起的呢?
三、 链接过程
每个全局变量和函数都可以看作是一个符号,存放在符号表中。 链接过程可以大致分为两步:
1. 符号解析 :将每个符号引用比如代码main.c 中的 sum , array 和符号表中的的符号定义关联起来 。
2. 重定位: 将可重定位目标文件组合起来,将所有相同类型的节合并。 将运行时的内存地址赋给新的聚合节,把符号定义和一个内存地址相关联。 然后修改这些符号的引用, 使他们指向这个内存位置。通过重定位段 .rel.text 和.rel.data 生成 已重定位的.text节和.data 节。
3. 加载到内存之后, .text 为只读内存段,.data 和.bss为读/写内存段。
四、 加载可执行目标文件
程序复制到内存运行之后的内存布局如上图所示。 刚才我们提到.data和.bss节中保存全局和静态变量, 而局部变量在运行时保存在栈中。