本篇文章目录
- 1. 过程概览
- 2. 编译与链接
- 2.1 预编译/预处理
- 2.2 编译
- 2.3 汇编
- 2.4 链接
- 3. 执行/运行环境
1. 过程概览
编译到链接是c语言的翻译环境,c语言还有执行环境。
- 组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code);
- 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序;
- 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
2. 编译与链接
2.1 预编译/预处理
总的来说都是文本操作,注释被删除替换成一个空格;#define定义的符号也会被删除,其它所有出现这个符号的地方都被替换成所代表的常量;头文件所有函数实现都会包含进来,总共大概一两万行,最后才是自己写的代码。
预编译后会产生一个对应的.i文件,不过要说明的是这个预编译的步骤在vs中可以看到,但后面的步骤基本只能在linux gcc编译器,使用指令一步步看。
在windows vs中查看预编译后产生的文件:
选择是然后运行程序,实际程序会编译失败,因为仅仅只进行了预处理这个步骤,不过可以在项目文件下的X64/Debug下看到一个对应的.i文件。
在linux下执行预处理选项指令 gcc -E test.c -o test.i(-E代表只进行预处理,-o test.i代表指定输出到这个文件),预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。
2.2 编译
在linux下执行编译选项指令 gcc -S test.c,编译完成之后就停下来,结果保存在test.s中。
编译阶段会将c代码转换为汇编代码,汇编大致会经过四个阶段:
- 词法分析;
- 语法分析;
- 语义分析;
- 符号汇总。
比如图中左边的这段代码,对应右边编译器一个个符号分析它们分别代表什么。
语法分析阶段会产生一个语法树:
语义分析应该是根据语法树进行,我也不是很了解。
符号汇总这个阶段很重要,编译器会将每个.c文件中的全局符号(函数名、全局变量)汇总起来,后面的汇编和链接都会涉及到符号的处理。
2.3 汇编
汇编阶段是将编译阶段产生的.s文件(汇编代码)转成二进制机器码文件。在linux下执行汇编指令 gcc -c test.c,汇编完成之后就停下来,结果保存在test.o中。
在这个阶段会根据前面编译阶段的符号汇总,产生一个符号表,如:
对于右边申明的Add函数,在符号表中的地址是无效的,后续不会使用这个无效的地址,而是使用左边的地址,符号表在连接阶段会使用。
2.4 链接
链接阶段链接器(linker)会链接所有产生的.obj / .o目标文件,以及链接库生成合并成一个二进制的.exe可执行程序文件,这个阶段会经历合并段表、合并符号表操作。
gcc编译器生成的二进制目标文件和二进制的可执行文件,都是按照elf文件形式组织的,合并段表要做的就是将所有.o文件中相同数据段进行合并在一起;每个文件产生的符号表最后也会合并在一起,符号表中无效的地址会被舍弃,使用有效的地址,因为链接器链接查找函数都是根据符号和地址的匹配关系去找的,不然没法找到;最后生成一个.exe文件。
add.c和test.c的符号表,其中0x0000,无效的地址值会被舍弃:
如果add.c中没有Add函数,那么符号表中没有对应的函数名Add和地址,那么会连接失败:
3. 执行/运行环境
- 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
- 程序的执行便开始。接着便调用main函数。
- 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
- 终止程序。正常终止main函数;也有可能是意外终止。