程序的翻译环境和执行环境
在ANSI C的任何一种实现中,存在着两种不同的环境。
也就是说,我们写出C语言的代码想编译成可以运行的程序,就必须要经过这两种环境。
第一种是翻译环境,在这一环境中源代码被翻译成可执行的机器指令。
另一种是编译环境,用来实际执行代码。
翻译成图大概就是这个样子。
其中,test.exe当然就是可执行的二进制指令,也被称为上面所说的机器指令。
详解编译+链接
翻译环境
我们知道,一个程序可以有许多源文件。编译器可以将这众多的源文件转换为目标代码。
而要把这众多的目标文件链接到一起,就需要链接器(linker)来大展身手了!
链接器可以将众多的目标文件捆绑到一起,形成一个单一而完整的可执行程序。
我们可以搜索到编译器和链接器。
图一是编译器,图二是链接器。
链接器同时也会引入标准C函数库中被该程序所用到的函数,它还可以搜索程序员个人 的程序库,并且将其需要的函数也链接到程序中。
我们想观察到编译和链接的过程,就需要用到gcc编译器,为什么不能用VS呢?
因为VS是一个集成开发环境,集成很多功能,一个ctrl+f5直接出结果了,不方便观察编译和链接细节。
编译本身也分为几个阶段
预编译(预处理)
翻译环境要经过编译和链接两个阶段,编译的过程又分为预编译,编译和汇编。
画成图的话就是下面这样(我 的 肝 好 累)(;´༎ຶД༎ຶ`)
我们先在gcc编译器搭建好环境。
我们想让test.c文件只是预编译,而不执行后面的操作,那就要这么写命令行。
gcc test.c -E -o test.i
这样我们就将预编译的内容写到了一个叫test.i的文件里面。
只写gcc test.c -E -o的话,上面那一长串代码就会打印到终端上面。
我们在这一串代码中发现了老朋友头文件stdio.h的身影。
那就引出了预编译的第一个功能——头文件的包含。
上面是test.i文件里存放的预处理之后的代码。我们发现,SZ被替换成了10,也是它被定义的值。
我们就发现了预编译的第二个功能——#define所定义的符号的替换。
预编译的第三个功能就直接写出了——注释的删除
上面的三个文本操作确实会让代码的行数减少很多。
编译
要观察编译的过程,我们写出这样的命令行。
运行命令,发现跳出一个叫test.s的文件。
这个文件里的内容和汇编指令十分相似。
所以编译阶段所做的事情就是把C语言代码翻译成汇编代码。
当然这之间的事情十分的复杂,要经过语法分析,词法分析,语义分析,符号汇总的操作。
大家可以去购买相关的书籍查阅,像《程序员的自我修养》里面就有相关的解析。
其中的符号汇总我下面会讲。
汇编
我们接着写出这样的命令行 gcc test.s -c。
弹出一个 test.o 文件。
在VS编译器上面,目标文件的后缀是.obj,而在gcc编译器,目标文件的后缀是.o。
所以test.o这个文件就是目标文件了。
目标文件是什么格式呢。
我们点开这个文件,发现它是二进制文件。
我们就明白了,汇编所做的事情就是将编译出来的汇编代码转换成二进制指令。
链接
链接过程就是将众多上面的test.o文件编译成test.exe,也就是可执行程序。
输入gcc test.o来直接编译test.o文件。
生成一个a.exe文件。
直接运行a.exe文件。
同样可以达到test.exe的效果。
上面说了, 链接过程就是将众多上面的test.o文件编译成test.exe,也就是可执行程序。
那如何编译呢?要经过两个过程:
1.合并段表
2.符号表的合并和重定位。
我就以下面的代码为例来讲解。
我们已经知道,上面的代码经过汇编会产生两个 .o 文件,我们就假设一个为Sub.o,另一个为test.o。
什么是符号汇总?
在编译阶段,程序会进行符号汇总的操作。
将函数声明和全局变量等进行汇总,其实就是收集起来。如图所示。
当完成符号汇总操作之后,程序就该进入汇编阶段了。
汇编阶段就是将编译出来的汇编代码转换成二进制指令。
这里面就包含了形成符号表的操作。
我们给收集来的符号假定几个地址,其中test.o里面的Sub给上空指针。
接下来就该合并段表和符号表的合并和重定位了。
合并段表
各目标文件需要链接库来形成可执行程序。
目标文件,已经是二进制的文件。它们是有格式的。
就以gcc编译产生的目标文件为例,目标文件的格式是elf的文件格式。
可支持程序文件的格式也是elf的。
这种文件格式会把文件分成段。
编译器就把这对应段的文件合并在一起,形成可执行程序。
当然,这一块的操作是非常复杂的。
符号表的合并和重定位
Sub.o文件和test.o文件各有各的符号表,难道生成的文件里也是各有各的文件表吗?
当然不是,它们要进行符号表的合并和重定位操作。
无效的地址会被清除,从而形成一个新的符号表。
它有什么用呢?
当我们删除声明的Sub函数,读取的符号表会变成main函数和空指针的Sub函数声明
我们通过这个空指针函数声明去找未定义的Sub函数时,就会报出链接时错误——无法解析的外部符号。
如果函数的名字写错也是同理。
总结
感谢观看,本文到这里就结束了,如果觉得有帮助,请给文章点个赞吧,让更多的人看到。🌹 🌹 🌹
也欢迎你,关注我。👍 👍 👍
原创不易,还希望各位大佬支持一下,你们的点赞、收藏和留言对我真的很重要!!!💕 💕 💕 最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!下期再见。🎉