《秋鳞小故事——编译器》
编译器工作流程:预处理—>编译—>汇编—>链接
=========================================================================
角色设定:
程序员:作家
程序源代码:文学作品
编译器:译者,负责将作品推向全球(在不同CPU上运行)。
(请注意分号“;”间隔,每个“;”代表一个功能的说明)
---------------------------------------------------------------------------------------------------------------------------------
一、预处理:
1.就是将参考书目的参考了哪些内容罗列出来(处理#include);
2.并且把著作稿件上的笔记和批注都去除(删除注释);
3.并且把作者文本中的简写和暗号都换成正式文本,比如把“NB”替换成“厉害”(展开宏);
4.然后因为写的时候是有草稿的,可能因为作者的思路写了很多不同剧情,预处理会根据作者的选择把需要的章节才放进最终稿件中,舍弃其余的(条件编译#ifdef
);
5.自动在稿件上盖上“写作日期”、“书名”等出版社印记(处理预定义宏,如__DATE__
, __FILE__
);
二、编译:(词法—>语法—>语义—>生成中间码/优化—>生成.s汇编文件—>输出.s汇编文件)
1.词法分析阶段
①编译器需要先检查文学作品中的字词、标点符号对不对(识别非法字符);
②以及多少个词才算是一个词(将字符序列分割成一个个独立的词法单元Token,输出[标识符][运算符][标识符]
这样的Token流)。
如“靓仔吃什么”这句话如果不进行词法分析,而直接进入语法分析,那就是“靓”“仔”“吃”“什”“么”“?”,进行词法分析之后就是“靓仔”“吃”“什么”“?”;
2.语法分析阶段
①编译器就会检查这些词凑出来是不是一句通顺的话(“说的词都对,但拼在一起我就看不懂了,这不符合语法规则吗”),检查结构是否正确(例如:是否缺少分号、括号是否匹配)。分析后会生成一棵抽象语法树(AST),直观地表示代码的结构;
3.语义分析阶段
①编译器会检查文学作品中是不是突然蹦出来一个之前从来没有说过的人或者剧情(变量未声明),或者让一个哲学家去做裁缝的工作(类型不匹配,如int a = "hello";
),存在这种情况就会语义报错。此阶段会装饰AST,添加类型等信息;
4.生成中间码/优化阶段
①编译器就会把文学原著翻译成不同国家的翻译家都看得懂、但却是不属于任何一国的语言(生成一种与机器无关的中间表示,如LLVM IR、GIMPLE);
②并将其中的长难句简洁化(进行各种优化,如:常量折叠把10+5
算成15
;死代码消除删除永远不会执行的代码;循环优化提升循环效率);
5.目标代码生成
①这是编译阶段的最后一步。编译器将优化后的中间表示(世界语剧本)转换(降低)为特定目标CPU的汇编代码(.s
文件)。例如,为Intel CPU生成x86汇编代码,为手机CPU生成ARM汇编代码;
输出:对应特定CPU架构的汇编代码文件(.s
文件);
三、汇编
①转换:
特定语言的翻译家(如x86汇编专家)拿到了那份汇编代码剧本,把它一对一地、机械地翻译成最终能给当地演员(CPU)看的、由0和1组成的纯机器码指令(将助记符如MOV
、ADD
转换为二进制操作码);
* 细节补充:这个过程几乎是查表式的。汇编器内部有一个指令表,它读取每一条汇编指令,比如 mov eax, 15
,就去表里找到这条指令对应的二进制操作码(例如 0xB8
),然后将操作数(15
)转换成二进制,最后组合成一个完整的二进制指令序列(例如 B8 0F 00 00 00
)。
②输出:
生成目标文件(.o
或.obj
文件),里面是几乎可以执行的机器码,但还可能缺少一些外部元素的地址(比如调用了其他文件中的函数);
* 细节补充:这个目标文件里不仅仅有机器码,还有一个非常重要的东西叫重定位表。这个表就像一份“待办事项清单”,专门记录着哪些指令里的地址是暂时空缺的、需要链接器来填补的。比如,call printf
这条指令在目标文件里,printf
的地址是先写为0的,旁边会有一个备注:“此处需要填入printf
函数的最终地址”。
四、链接
①合并:
导演(链接器)把本书所有章节编译出的角色戏份(多个.o
目标文件)、以及从经典文库(如C语言标准库libc.a
)中借用的标准剧情(库文件),全部合并到一起;
* 细节补充1(合并节):链接器会做一个叫做“合并节”的操作。它会把所有目标文件里的代码部分(比如 .text
段)拼接到一起,把所有数据部分(比如 .data
段)也拼接到一起,形成最终程序的一个个连续的内存段。
②解决地址引用:
确保所有角色互动正确(解决模块间的函数调用和变量访问),为所有函数和变量分配在最终剧本(内存)中的确切出场地址(绝对地址);
* 细节补充(重定位):这是链接器最核心的工作。它现在知道了所有符号的最终地址,于是它开始处理第一步中提到的那个“待办事项清单”(重定位表)。它找到 call printf
这条指令,把之前写为0的地址,替换成printf
函数在内存中的真实地址。这个过程就叫重定位
③输出:
最终合成一本完整的、可上演的最终剧本(可执行文件,如a.out
或.exe、.hex
);
* 细节补充(文件格式):链接器输出的文件格式通常是操作系统可识别的格式,如Linux下的ELF格式或Windows下的PE格式。这些格式不仅包含了机器码,还包含了告诉操作系统如何加载这个程序的信息(例如:从哪里开始执行、需要多少堆栈空间等)。