假如在Linux系统终端,我们创建了一个.c文件,如:main.c,我们可以通过以下指令来运行它。
gcc -o main main.c
这个时候会增加一个叫做 main 的文件。然后输入下一条指令:
./main
该C程序就会运行。
那么问题来了:为什么main.c文件不能直接运行?从main.c到main这一过程发生了什么?接下来,就让我们探究一下。
首先,我们要了解的是,上述运行过程其实包含了四个步骤,分别是:预编译(prepressing)、编译(complication)、汇编(assembly)、链接(linking)。接下来我们会对这四个过程进行具体分析。
一、预编译(prepressing)
我们首先创建一个C程序文件:love_01.c 其内容为:
#include<stdio.h>
int main()
{
int a = 2;
int b = 5;
printf("a + b = %d\n", a + b);
return 0;
}
然后我们通过以下命令只对其进行预编译:
gcc -o love_01.i -E love_01.c
这时候我们生成了一个 .i 文件,这就是对C程序文件进行预编译后生成的文件,那么接下里我们看看这个文件里是什么内容:
我们发现,这个文件里总共有857行,但只有最后的几行是我们所写的代码,那前面这一大堆是什么来头呢?自然地,我们在main函数前面写的只有一个 #include<stdio.h> 而已,那前面这800多行难道是这个头文件的展开?
为了验证这一猜想,我们另外创建一个 love_02.c 的C程序文件,其内容为:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int a = 2;
int b = 5;
printf("a + b = %d\n", a + b);
return 0;
}
与love_01.c只有一处不同,就是多个#include<stdlib.h>,接下来还是按照老办法,只对其进行预编译,并查看生成的love_02.i文件.
我们发现,仅仅是加了一个头文件,预编译后就从857行增加到了1847行,而我们main函数的程序,还是只占了寥寥几行而已,结果不言而喻。
接下来,我们再创建一个love_03.c文件,其内容为:
#include<stdio.h>
#include<stdlib.h>
#define c 0 //I am a student
int main()
{
int a = 2 + c;
int b = 5 + c; //How are you?
printf("a + b = %d\n", a + b + c); //I am fine,thank you,and you?
return 0;
}
这里我们是在love_02.c 的基础上添加了一个宏定义,#define c 0 和一些注释,这样预编译后的文件love_03.i会是什么样子呢?如下图所示:
我们惊奇地看到,其行数并没有发生改变,并且我们的宏名c也被替换成了0,注释也不见了。所以,预编译的过程中发生了什么呢?这里我用《程序员的自我修养》这本书里第39页的内容总结:
预编译过程主要处理那些源代码文件中以“#”开始的预编译指令,其主要处理规则如下:
1、将所有的“#define”删除,并且展开所有宏定义
2、处理所有的条件预编译指令,如:“#if” “#endif”,“#ifdef”“#elif” “#else”
3、 处理“#include”预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的头文件可能还包含其他文件
4、 删除所有的注释“//”和“/**/”
5、 添加行号和文件名标识,比如上面的#3 “love_03.c” 2 ,以便于编译时编译器产生调试用的行号信息以及用于编译时产生编译错误或警告时能够显示行号
6、 保留所有的#pragma编译器指令,因为编译器需要使用它们
二、编译(complication)
还是使用love_01.c这个文件进行说明,刚才经过预编译这一过程,我们当前文件夹里已经有了love_01.c 和 love_01.i 这两个文件,接下来我们使用这条命令对 love_01.i 进行编译:
gcc -olove_01.s -S love_01.i
然后生成love_01.s这个文件,就是编译后的结果,让我们来看一下里面是什么:
我相信如果学过汇编的同学对这些一定不会陌生,因为这就是汇编代码!
所以编译这一过程,是把预编译后的文件进行一系列词法分析、语法分析、语义分析及优化后生成的相应的汇编代码文件。
三、汇编(assembly)
还是继续针对love_01.c说明,我们当前有.c .和 .i以及 .s三个文件,接下来我们用以下指令对love_01.s进行汇编
gcc -o love_01.o -c love_01.s
生成的是love_01.o文件,让我们看看里面是什么内容吧:
这里面的内容让我们一头雾水,根本看不懂,这是很正常的,因为这些就是只有机器能读懂的机器指令。
通过汇编这一过程,将汇编代码转变成机器可执行的指令,每一个汇编语句几乎都对应一条机器指令。
而经过前面三个步骤:预编译,编译,汇编生成的 .o 文件我们叫做目标文件(object file)
那么现在,机器能识别这些机器指令了,是不是就能够运行了呢?很可惜,是不能的。那么原因是什么呢?上帝视角的我们自然知道还有一步链接的过程没进行,那么链接到底是什么?明明机器指令已经有了,为甚么计算机还是不能运行这个文件?目标文件和可执行文件的差别到底在哪里?
四、链接(linking)
链接这里是十分重要的知识点,首先发我们要知道目标文件里有什么,链接过程又做了那些事情,这些内容我会在之后的博客中进行详细补充,此处暂且不表,有兴趣的同学可以翻阅《程序员的自我修养》这本书。
1、
2、
。