C语言学到了现在,相信大家已经掌握了相当多的知识,会写很多程序,但是有一个问题始终没有解决,那就是:一个程序是如何运行起来的,在实现的过程中都发生了什么?在这篇博客中,将会为大家解决这些问题
程序的实现
在ANSI C的任何一种实现中,都存在两个不同的环境,一种是翻译环境,在这个环境中源代码会被转换成可执行的机器指令。另一种是执行环境,它负责把代码运行出来,画图理解如下:
test.c代码如下:
#include<stdio.h>
int main()
{
printf("1 2 3 4 5 6 7 8 9 10\n");
}
可以看到这个代码很简单,就是打印一到十,但是这个打印结果是怎样呈现在我们的电脑上的,过程是什么样的?这就是接下来我们要说的重点:
首先,我们写的这几行代码就是源文件,叫test.c,但是这个test.c是不能直接被执行的,这个源文件要经过层层转化,变成.exe(可执行文件)才能够运行,那么这个过程就叫翻译环境。
我们可以看看啊
在储存项目的文件夹里面,首先是有一个test.c,这是我们的源文件,源文件很小啊,只有1KB。
在另一个文件夹中储存着经过转化后的可执行程序文件,足足有61KB,那在这个转化中到底发生了什么呢?
翻译环境
整个翻译流程的过程是这样的,实际上计算机可以多个程序同时编译,就像这样:
这样的过程多半在多个源文件,函数嵌套使用的时候会用到这个我们之后再说。
我们可以看到,源文件经过编译器的处理变成目标文件,目标文件通过链接器的处理形成可执行程序,那在编译和链接的过程中,发生了什么呢?
编译
其实在Linux环境中翻译的每一步都能呈现出来,但是由于笔者的Linux并不是很好,所以这里用VS替代。
预处理阶段
可以看到,编译也是分为三个阶段的,先看第一个预处理阶段,这个阶段会发生什么事情呢,第一点是头文件的包含,我们在写代码的时候,都会引用标准库,也就是#include<stdio.h>,光写这个代码是没有作用的,但是经过了预处理以后我们会把整个库都放到你的代码里面,你的代码突然就多了几百甚至上千行,感兴趣的可以自己在gcc中模拟一下。
第二点是宏定义符号的替换,也就是#define ,在写程序的时候,我们经常用 #define n 10这样的语句进行替换,但其实,n在预处理的时候,会被编译器重新替换成10.
可以看到,预处理之后程序发生了明显的变化,宏定义全被替换了,注释也没了,还会把stdio引用进来,这代码量一下子就上来了。
编译阶段
在编译阶段,编译器会根据一些规则,把你写的C语言代码,转换成汇编代码。
是这里是对你的语法,词法,语义进行分析,转化成汇编代码,这些如果学过编译原理的同学应该会很了解,但是编译阶段最重要的还是符号汇总
这个符号汇总呢,会统计一些全局的符号,例如main,又例如全局出现的函数名之类的,变量名不属于其中的范围,下面我们看个例子:
extern是一个关键字,说明该函数不是在这里声明的,去其他文件里边找。
看看这里的符号有哪些,在 test中,符号: add、main
在add中,符号:add
在编译阶段,我们会把这些符号先收集起来备用。
汇编
在汇编阶段,我们会把汇编指令转换成计算机所能识别的二进制指令,我们可以看看这些指令:
这是test.obj文件,我们是完全看不懂的,在转换的时候,还会形成符号表,就是在编译阶段的那个符号汇总。
这就是test和add的符号表,可以看到他们是有重复的,而且test.obj中的add是没有地址的,因为就不是从他这里声明的,所以地址为空啊,这里的地址只是举个例子,并不一定准确。
到这里,编译阶段就完成了。
链接
紧接着是链接阶段,链接阶段主要就做两件事,一件是合并段表,另一件事是符号表的合并与重定位:
这个过程并不难理解啊,先说说合并段表,在经过编译时,文件会从.c文件转化成.obj的二进制文件,每个二进制文件被分成了很多个段,代表着不同的功能,而在链接阶段要做的就是把这些段连起来
有点类似于这样,凑活看看。
链接的时候还会完成符号表的合并和重定向,这又是什么意思呢?
符合表的合并,那就是把多个文件的符号表合并成一个,在这个过程中,重复的会被合成一个,而且会重新寻址,之前说的没有声明的函数没有地址,在这里也会找打它被声明的文件,给上它一个地址,合并完大概是这样(还是add的那个例子)
就是这么一个效果,后面跟的是他的地址。
那么经过这个操作以后,.obj文件就成功的转化为了.exe的可执行文件。
碎碎念
由于笔者的能力问题,并没有深挖细节,十分惭愧,来日方长,学明白以后一定再深入细致的说明这些过程。