程序代码到可执行程序编译链接过程
预编译
以c++/c 语言为例,预编译阶段的工作有以下几点:
- 处理所有#define 及条件预编译指令(如 #if,#ifdef.....),并展开所有宏定义。
- 删除所有注释("//" ,"/**/")。
- 处理 "#include",将被包含文件插入该预编译指令位置。(整过过程递归进行,因为被包含文件也可能包含其他文件)
- 添加行号与文件标识。(用于调试时产生的编译错误及报错等信息)
预编译过程相当于如下命令:
gcc -E hello.c -o hello.i (-E 表示只进行预编译)
或者
cpp hello.c > hello.i
编译
编译过程可以分为如下步骤:
-
扫描
-
词法分析
运用一种类似于有限状态机的算法,将源代码的字符序列分割为一系列记号(关键字、标识符、字面常量、特殊符号等)。【一个名叫lex的程序可以完成这项任务】
-
语法分析
对由扫描器产生的记号进行语法分析,进而产生语法树。(采用上下文无关的语法分析手段)【同样一个叫做yacc的工具也可完成这项任务】
-
语义分析
包括静态语义(如声明和类型的匹配、类型的转化等)和动态语义(运行阶段才能确定)。
-
源代码优化【这阶段也包括中间代码(例如llvm 中的 IR)的生成】
由于直接在语法树上作优化难度较大,源代码优化器通常将语法树转化为中间代码,再进行优化。
-
目标代码生成和目标代码优化
代码生成器将中间代码转化成目标机器代码。
接着目标代码优化器对上述目标代码进行优化。(如选择合适的寻址方式,删除多余指令等)
编译过程相当于如下命令:
gcc -S hello.i -o hello.s (.s 是汇编输出文件的后缀)
或者
gcc -S hello.c -o hello.s (预编译和编译合并了)
汇编
汇编器将汇编代码转变为机器可以执行的指令。(生成可重定位文件 .o)
编译过程相当于如下命令:
as hello.s -o hello.o
或者
gcc -c hello.s -o hello.o
或者
gcc -c hello.c -hello.o (上面三个过程一步完成)
链接
对于一个复杂的软件,将每个源代码模块独立地翻译,然后组装。这个组装模块的过程就是链接。(主要包括地址和空间分配、符号决议、重定位等步骤)
最基本的静态链接过程:每个模块的源代码文件(如.c)文件经过编译器编译成可重定位文件(Object File,扩展名为.o或.obj),可重定位文件和库一起链接形成最终可执行文件(.out)。
链接过程相当于如下命令:
gcc hello.o -o hello.out
以如下代码为例:
#include<stdio.h>
int main()
{
printf("hello world");
return 0;
}
预编译(hello.i) | 编译(hello.s) |
---|---|
汇编(hello.o) | 链接(hello.out) |
可重定位文件 [.o 或 .obj]
可重定位文件的格式
目前PC平台流行的可执行文件格式(Executable)主要是:
PE(Windows)和 ELF(Linux)。【两者都发源自 COFF 可执行文件格式】
另外的如ios 是 Mach-O格式,android 是dex格式。
而可重定位文件是源代码编译后但未进行链接的中间文件。(Windows 下的.obj 和 Linux 下的.o)。
因此,可重定位文件和可执行文件的内容和结构是很相似的。(可以广义的将二者看作一种类型的文件)
同时动态链接库(Windows 下的.dll 和 Linux 下的.so)和 静态链接库(Windows 下的.lib 和 Linux 下的.a)文件都可按照可执行文件格式存储。
【小技巧: Linux 下可使用file命令查看相应的文件格式】
程序的指令和数据分开存放的好处:
- 程序装载后,数据和指令分别映射到两个虚存区域。数据区域对进程而言是可读写的,指令区域对于进程而言是只读的。这样可以防止程序指令被有意或者无意地更改。
- 利于提高程序的局部性。(提高缓存的命中率)
- 当系统中运行着多个该程序副本时,内存中只需要保存一份该程序的指令部分。(最重要的原因)