我们平时写的.c源码,电脑是看不懂的。它只能识别机器码。从源码到最后能点开运行的exe,一共四步:预处理 → 编译 → 汇编 → 链接。
第一步:预处理
输入是.c文件,输出.i文件。 这一步不会检查语法,就做文本替换。 处理所有#开头的东西:#define宏展开、#include把头文件内容塞进来、删掉注释、处理#ifdef这种条件编译。 举个例子,写了#define NUM 10,预处理的时候,代码里所有NUM全部直接换成10。 这一步结束之后,得到的还是C风格的源码,只是干净很多,没有宏、没有注释。
第二步:编译
输入预处理后的.i文件,输出汇编代码.s。 这一步才开始做语法检查。括号没配对、分号漏写、变量没声明,都是这一步报错。 编译器把C语言翻译成汇编指令。这个阶段只看当前这一个文件,别的源文件它不管。 只要你写了函数声明,就算这个函数的实现不在当前文件里,编译也能通过,不会报错。
第三步:汇编
输入汇编.s文件,输出目标文件.o(windows是.obj)。 汇编器把一条条汇编指令转成二进制机器码。.o里面已经是机器指令了,但是它是残缺的。里面调用别的文件里的函数时,只留了一个空位,不知道这个函数在内存哪个地址。
第四步:链接
把多个.o目标文件,加上系统库拼在一起,生成最终可执行程序。 这一步就是很多坑的来源。 编译只查有没有声明;链接才去找函数真正的定义。 比如你写了void test();声明,但是从头到尾没写test函数的代码。单个文件编译能过,到链接的时候就报错:undefined reference。 链接器干两件主要的事:
符号解析:找到每个函数、全局变量对应的实体
重定位:把之前目标文件里留空的地址,填上真实地址
链接分静态链接、动态链接。课上主要讲静态链接,就是把库的代码直接打包进exe,程序拿到别的机器也能跑。动态链接是运行的时候才去加载系统库,文件更小。
一个很经典的例子,区分编译错和链接错
文件a.c
#include <stdio.h> void fun(); int main() { fun(); return 0; }这里只声明了fun,没有写fun的实现。 单独编译a.c,没问题,可以生成a.o。 等到链接的时候,找不到fun的实体,直接链接报错。
如果我把声明删掉,main里面直接调用fun(); 没有声明。这一步在编译阶段就报错,因为语法检查没过。 记住一句话:编译看声明,链接找定义。这个选择题考超级多次。
程序加载运行
链接完成得到可执行文件,但它还不能直接跑。操作系统要把这个程序加载进内存,找到入口,才开始执行。很多人以为入口是main,其实不是,main只是给我们写业务代码的地方,真正的入口是系统给的启动代码,它做好准备之后,才去调用main。
简单总结
预处理:换宏、插头文件、删注释(文本处理) 编译:C→汇编,查语法 汇编:汇编→机器码,产出.o 链接:拼所有.o和库,补齐函数地址,生成可执行文件