《程序员的自我修养》学习笔记——揭秘源文件到可执行文件的编译过程【第一弹】

程序代码到可执行程序编译链接过程

预编译

以c++/c 语言为例,预编译阶段的工作有以下几点:

  1. 处理所有#define 及条件预编译指令(如 #if,#ifdef.....),并展开所有宏定义。
  2. 删除所有注释("//" ,"/**/")。
  3. 处理 "#include",将被包含文件插入该预编译指令位置。(整过过程递归进行,因为被包含文件也可能包含其他文件)
  4. 添加行号与文件标识。(用于调试时产生的编译错误及报错等信息)
预编译过程相当于如下命令:

gcc -E hello.c -o hello.i  (-E 表示只进行预编译)
或者
cpp hello.c > hello.i
编译

编译过程可以分为如下步骤:

image

  1. 扫描

  2. 词法分析

    ​ 运用一种类似于有限状态机的算法,将源代码的字符序列分割为一系列记号(关键字、标识符、字面常量、特殊符号等)。【一个名叫lex的程序可以完成这项任务】

  3. 语法分析

    ​ 对由扫描器产生的记号进行语法分析,进而产生语法树。(采用上下文无关的语法分析手段)【同样一个叫做yacc的工具也可完成这项任务】

  4. 语义分析

    ​ 包括静态语义(如声明和类型的匹配、类型的转化等)和动态语义(运行阶段才能确定)。

  5. 源代码优化【这阶段也包括中间代码(例如llvm 中的 IR)的生成】

    ​ 由于直接在语法树上作优化难度较大,源代码优化器通常将语法树转化为中间代码,再进行优化。

  6. 目标代码生成和目标代码优化

    ​ 代码生成器将中间代码转化成目标机器代码。

    ​ 接着目标代码优化器对上述目标代码进行优化。(如选择合适的寻址方式,删除多余指令等)

编译过程相当于如下命令:
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)。

image

链接过程相当于如下命令:

gcc  hello.o -o hello.out 
以如下代码为例:

#include<stdio.h>

int main()
{
printf("hello world");
return 0;
}
预编译(hello.i) 编译(hello.s)
image image
汇编(hello.o) 链接(hello.out)
image image

可重定位文件 [.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命令查看相应的文件格式】

程序的指令和数据分开存放的好处:

  1. 程序装载后,数据和指令分别映射到两个虚存区域。数据区域对进程而言是可读写的,指令区域对于进程而言是只读的。这样可以防止程序指令被有意或者无意地更改。
  2. 利于提高程序的局部性。(提高缓存的命中率)
  3. 当系统中运行着多个该程序副本时,内存中只需要保存一份该程序的指令部分。(最重要的原因)
posted @ 2023-03-03 13:26  Only-xiaoxiao  阅读(212)  评论(0编辑  收藏  举报