从源代码到可执行文件
复习
- 函数:把一段工作命名、传入参数并获得结果
- 函数调用与运行栈:保存参数、局部变量和返回地址
- 程序在内存里的样子:代码区、数据区、栈和堆的初步全景
TL;DR
- 从源代码到可执行文件,中间要经过几道工序
- 编译:高级语言 → 汇编;汇编:汇编 → 目标文件;链接:多个目标文件与库 → 可执行文件
- 汇编器产出的“目标文件”里,除了机器码还有符号和待补齐的位置
- 编译器内部先当黑箱,后面讲编译原理时再打开
正文
我们把这条线索欠了很久:前面写的是高级语言,机器认的是机器码,中间那段“翻译”到底是怎么一步步完成的?
这一章,先把整条流水线看一遍。它有几道工序,每道工序负责一件事。
四道工序
源代码
│ ① 编译(编译器)
▼
汇编代码
│ ② 汇编(汇编器)
▼
目标文件
│ ③ 链接(链接器)+ 其它目标文件、库
▼
可执行文件
│ ④ 加载
▼
内存中运行的程序
- ① 编译 :把高级语言翻译成汇编代码。负责这步的程序叫 编译器 (compiler)。
- ② 汇编 :把汇编翻译成机器码,产出 目标文件 (object file)。这就是我们前面造过的那个汇编器干的活。
- ③ 链接 :把多个目标文件和用到的库拼在一起,产出 可执行文件 (executable)。
- ④ 加载 :把可执行文件装进内存,交给 CPU 运行。这一步是操作系统的活儿,后面再讲。
前三步里,编译是重头戏。它怎么把 max、while、表达式变成汇编,属于一块硬骨头,本系列到编译原理部分再专门打开。现在,我们只把编译器的输入输出看清楚。
为什么中间要有目标文件
既然汇编器能直接产出机器码,为什么不一步到位,还要中间折腾出一个“目标文件”,再链接?
因为真实的程序不是一个孤零零的文件:
- 它常常拆成好几个源文件, 分开编译 ,最后再拼起来
- 它还会用到别人写好的 库 (比如打印、开方这些现成功能)
分开编译有个绕不开的麻烦:编译某一个文件时,我们并不知道其它文件里的函数最终会被放在哪个地址。链接器的工作,就是把这些“悬着的引用”一一接上。
所以目标文件里装的,除了机器码,还有:
- 符号表 :这个文件里定义了哪些名字、引用了哪些名字(还记得汇编器的那张表吗?)
- 待补齐的位置 :哪些地方的地址还空着,需要链接时填上
链接器把所有目标文件摆到一起,给每个符号定下最终地址,再把空着的地方补好——这个过程叫 重定位 。拼完之后,一个完整的可执行文件就诞生了。
每一步都在加一层
回头看,这又是“加一层”的老故事:
人写: 高级语言
翻译: 编译器 → 汇编 → 汇编器 → 目标文件 → 链接器 → 可执行文件
机器跑: 取指、译码、执行
人只写最上面那一层,下面每一层都由一个专门的程序承包。是谁在替我们管寄存器、管地址、管多个文件之间的引用?正是这条流水线。
不过,还有一个岔路口没有交代:高级语言变成机器指令,除了“先整体翻译、再运行”,还有没有别的走法?下一章,我们对比两条路——编译和解释。
思考题
如果程序只有一个源文件、也不使用任何库,是不是就不需要链接,汇编完直接就能运行?想想看,一个“什么都不依赖”的程序,现实中有多常见。
小结
知识点
- 从源代码到可执行文件:编译、汇编、链接、加载
- 编译器:高级语言到汇编
- 目标文件:机器码、符号表、待重定位的位置
- 链接器:合并目标文件与库,完成重定位
- 每一步都由专门的程序负责
参考资料
- Wikipedia(zh):编译流程:编译、汇编与链接
- Wikipedia(zh):目标文件:目标文件里有什么
- Wikipedia(zh):链接器:符号解析与重定位
- 《深入理解计算机系统》第 7 章:链接
思考题答案(仅供参考)
理论上可以,但现实中极少。哪怕只写一句输出,也要用到操作系统提供的功能或标准库;程序启动时也需要一段“入口”代码。换句话说,几乎任何能跑的程序,都要和别的代码拼在一起,链接这一步基本省不掉。
协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
封面图
设计师 | 南国微雪