我们所写的程序要形成一个可执行文件中间可是要经过复杂的过程的:
1.组成一个程序的每个源文件通过编译过程分别转换成目标代码。
2.每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序。
3.链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链 接到程序中。
我们写好的源文件运行成为一个可执行程序的过程:
.c源程序————>预编译(.c)————>编译(.s)————>汇编(.o文件)————>链接(.exe程序)
一、预编译
1.宏定义指令:对于#define M 10这样的宏,预处理时就将程序中所有的M用10替换;
对于#undef这样的宏,预处理时就取消对宏的定义,使以后出现的宏不被替换。
2.展开头文件:将写好的头文件或者库里已经有的头文件包含到该源文件中,就可以直接使用,体现了程序的模块化也更方便。
#include<math.h>:告诉编译器去系统默认的路径寻找math.h文件。
#include"math.h":告诉编译器先去该源程序所在目录查找math.h,如果没找到再去系统默认的路径寻找。
3.去掉注释:注释为了读的人方便理解,预处理阶段就去掉了,并不参与编译。
4.条件编译:预编译根据有关的条件编译指令,可以决定对哪些代码处理,对哪些代码过滤掉,从而减少编译时间提高效率。
二、编译
将预处理完的文件进行词法分析,词意分析,语法分析,符号汇总以及优化后形成相应的汇编代码。
编译器在每个文件中有一张函数地址符表,表中存储函数的地址。
三、汇编
将汇编代码翻译成目标文件(机器码)。目标文件由段组成,一般情况下分为代码段和数据段。
代码段:包含程序的指令,不可写可读可执行。
数据段:存放程序中的全局变量,静态数据。可读可写可执行。
四、链接
当工程量大,往往会写多个源文件,那么这些源文件在经过上述过程后要通过链接器链接在一起才能被执行。
编译器将每个文件中call指令后面的地址补充上,从当前文件的函数地址符表开始找,如果没有找到,就去别的文件的地址符表找,找到后填充在call指令的后面,如果找不到,编译器会报链接错误。
举例:编写一个test.c文件:
#include <stdio.h>
int main()
{
int i = 0;
for(i=0; i<10; i++)
{
printf("%d ", i);
}
return 0;
}
1.预处理后的文件保存在test.i文件中,查看一下test.i文件里的内容,呃,并看不懂。其实这就是一些头文件的展开,宏替换删除 注释呀。
gcc -E test.c -o test.i
2.编译:用下述命令编译,编译的内容放在test.s文件中,查看一下test.s的内容,会发现有eax,edi等寄存器,那么这看起来就是汇编代码了。正是如此,变异的过程就是通过一系列的检查后将语言代码转化成汇编代码。
gcc -S test.c
3.汇编:汇编完成之后就停下来,结果保存在test.o中。这是一堆看不懂的机器指令,计算机要运行程序就要转换成机器指令,因为别的太复杂了计算机看不懂啊,汇编就是将汇编代码转化为机器指令。
gcc -c test.c