编译到链接是c语言的翻译环境,c语言还有执行环境。
总的来说都是文本操作,注释被删除替换成一个空格;#define定义的符号也会被删除,其它所有出现这个符号的地方都被替换成所代表的常量;头文件所有函数实现都会包含进来,总共大概一两万行,最后才是自己写的代码。
预编译后会产生一个对应的.i文件,不过要说明的是这个预编译的步骤在vs中可以看到,但后面的步骤基本只能在linux gcc编译器,使用指令一步步看。
在windows vs中查看预编译后产生的文件:
选择是然后运行程序,实际程序会编译失败,因为仅仅只进行了预处理这个步骤,不过可以在项目文件下的X64/Debug下看到一个对应的.i文件。
在linux下执行预处理选项指令 gcc -E test.c -o test.i(-E代表只进行预处理,-o test.i代表指定输出到这个文件),预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。
在linux下执行编译选项指令 gcc -S test.c,编译完成之后就停下来,结果保存在test.s中。
编译阶段会将c代码转换为汇编代码,汇编大致会经过四个阶段:
比如图中左边的这段代码,对应右边编译器一个个符号分析它们分别代表什么。
语法分析阶段会产生一个语法树:
语义分析应该是根据语法树进行,我也不是很了解。
符号汇总这个阶段很重要,编译器会将每个.c文件中的全局符号(函数名、全局变量)汇总起来,后面的汇编和链接都会涉及到符号的处理。
汇编阶段是将编译阶段产生的.s文件(汇编代码)转成二进制机器码文件。在linux下执行汇编指令 gcc -c test.c,汇编完成之后就停下来,结果保存在test.o中。
在这个阶段会根据前面编译阶段的符号汇总,产生一个符号表,如:
对于右边申明的Add函数,在符号表中的地址是无效的,后续不会使用这个无效的地址,而是使用左边的地址,符号表在连接阶段会使用。
链接阶段链接器(linker)会链接所有产生的.obj / .o目标文件,以及链接库生成合并成一个二进制的.exe可执行程序文件,这个阶段会经历合并段表、合并符号表操作。
gcc编译器生成的二进制目标文件和二进制的可执行文件,都是按照elf文件形式组织的,合并段表要做的就是将所有.o文件中相同数据段进行合并在一起;每个文件产生的符号表最后也会合并在一起,符号表中无效的地址会被舍弃,使用有效的地址,因为链接器链接查找函数都是根据符号和地址的匹配关系去找的,不然没法找到;最后生成一个.exe文件。
add.c和test.c的符号表,其中0x0000,无效的地址值会被舍弃:
如果add.c中没有Add函数,那么符号表中没有对应的函数名Add和地址,那么会连接失败: