c程序的翻译和执行环境
首先我们要知道在标准c中存在着两种不同的环境分别是程序的翻译环境和执行环境。那么这两种环境的功能是什么呢?
翻译环境
首先我们要知道一个或是多个源文件是怎么变成一个可执行程序的呢?
我们可以通过下面的这张图更加大概的了解一个源文件是怎么变成一个可执行程序的。
那么翻译环境究竟包含哪些步骤呢? 这就是翻译环境所做的大概简图:
我们知道在一个c语言工程中会包含很多个源文件,而每一个源文件经过编译器单独编译之后都会形成各自对应的目标文件(假设源文件为test.c那么生成的目标文件也就是test.obj),每一个目标文件和链接库经过链接器,之后形成可执行程序。
我们简单写一个代码看是否存在这样的文件。
#include<stdio.h>
extern int Add(int a, int b);//声明存在一个函数的名字为Add参数是两个整型
int main()
{
int a = 4,b = 5;
int c = Add(a, b);
printf("%d", c);
return 0;
}
这两个代码存在于两个不同的源文件中
int Add(int a, int b)//实现Add函数
{
return a + b;
}
现在我们就去工程文件里面去找是否真的生成了一个obj文件
从图片可以看出确实生成了obj文件,当然我是在vs2022的编译环境下,如果是在gcc的环境下生成的目标文件后缀就是.0。 现在我们就来较为详细的了解一个源文件是怎么编译成一个obj文件的。 接下来我会写一段很简单的代码,打印1到10的数字但是我将其放到gcc的环境下运行,同时运用命令行将源代码停止在预处理的阶段,让我们能够知道预处理究竟做了什么事情。
预处理
代码:
#include<stdio.h>
int main()
{
int arr[10]={1,4,7,8,5,2,9,6,3,10};
for(int i = 0;i<10;i++)
{
printf("%d ",arr[i]);
}
return 0;
}
输入这个命令行之后,我们会发现终端中打印了一大推的东西,但是很显然这并不便于我们去观察所以我们可以使用gcc sub.c(这里的名字是你所创建的c文件名字)-E -0 test.i 这个命令的运行之后就会将刚刚那一大串的东西放到名为test.i的文件中去便于我们观察。
我们会发现在最下面的任然是我们的代码,但是不同的是没有了包含stdio.h的那个代码,而上面的那一大串也就是stdio.h里面的文件,从这我们能看到预处理的其中一个功能就是将头文件内容包含进去。那么如果我们给这个代码加一个注释在执行之后注释会不会消失呢?
可以看到注释也消失了,由此可以得到预处理还会将注释消除掉,还有一个功能我这里就不试验了,那就是会将**#define定义的标识符常量进行替换**。小小的总结一下:预处理主要就是进行了文本方面的操作例如头文件的包含,注释的消除等等,都是在文本层面进行处理。主要和预处理的指令(#define,#include,#pragme pack等有关)
编译
使用的命令行就是gcc test.i(刚刚你所预处理后生成的文件名字) -S
可以看到生成了一个名叫test.s的文件,观察这些代码很容易就能知道这就是汇编代码,由此我们也就能够知道了编译的功能就是:将c语言代码翻译成汇编的代码,当然这只是说了最后的结果而已,真的要做到这一点其实还有很多步骤,例如词法分析,语法分析,语义分析,符号汇总,待会我会讲解符号汇总。但是其它的步骤因为我并没有接触过就不说了。
汇编
最后我们来看汇编这个功能使用的命令行是gcc test.s -c
可以看到左边生成了一个test.o的文件,这也就是目标文件,和上面的文件后缀不一样是因为我在最上面显示的那个文件(.obj文件)是vs2022的环境下编译执行的,而这里的文件(.o文件)是在gcc的环境下编译执行的。
点开这个文件会发现不让打开,因为这个文件里面是二进制的。 从这我们也就能知道了汇编的功能就是:把汇编代码转换成二进制的指令除此之外还有就是会形成一个符号表
链接
在完成了上面的代码之后,就到了最后一步那就是链接,链接的功能:在具有很多.c文件的项目中,经过上面的三步会形成多个.o文件,而链接就是将这些文件链接起来形成可执行程序。当然要完成这一功能肯定还有其它很多事要做例如合并段表和符号表的重定位
用一幅图总结一下上面的三个步骤究竟干了什么:
从图我们可以看到编译,汇编和链接功能都涉及到了符号,我们就来更加深入的了解一下这几个点。
符号
1.符号汇总和符号表的形成:在编译的时候会对要编译的代码进行一次分析查找全局的符号 例如在一个程序里面的函数名(main,我们自己所写的一些功能性函数),全局变量等等。 以下面的这个代码为例:
#include<stdio.h>
extern int Sub(int a, int b);//声明存在一个函数的名字为Add参数是两个整型
int main()
{
int a = 4,b = 5;
int c = Sub(a, b);
printf("%d", c);
return 0;
}
int Sub(int a, int b)//实现Add函数
{
return a - b;
}//这两个代码是在不同的源文件中的
那么编译器会怎么处理呢?
如果上面的代码里面含有全局变量的话,这个全局变量也会被记录到符号表里面。 然后我们就会进入到链接部分(合并段表和符号表的重定位)我们先来简单的理解一下什么是合并段表首先一个目标文件已经是一个二进制文件了,而每一个目标文件都是有格式的,以gcc环境下形成的目标文件为例子,这种目标文件的的格式为elf,我们就以上面的那个代码为例:
然后我们来讲解什么是符号表的合并和重定位。
我们知道每一个源文件都有自己的符号表,编译器会将两个符号表进行合成,如果有一个函数名字两个符号表都有,那么编译器就会选择将有效地址的Sub函数存入到新的符号表中,而这个新的符号表就包含了之前文件所有的符号。
而这也就解释了如果我们在一个源文件里面声明了一个Sub函数,但是如果我们在实现这个函数的时候打错了名字打成了Sud那么在形成符号表的时候,总的符号表里面就会出现两个函数,一个名叫Sub它的地址为无效的,一个名叫Sud它的地址为有效的,但是在执行程序的时候经过符号表的对照,编译器会进入到那个无效的地址中去,因为主函数调用用的任然是Sub函数,所以最后出错。
如果这篇博客对你有所帮助的话,我很高兴,如果你发现了错误的话,请严厉指出我一定改正。