目录
一、C语言的相关文件
在使用VS编译并执行程序时,观察工程所处的目录,可以发现主要有以下几类文件:
a.后缀为.c的文件 源文件
b.后缀为.obj的文件 目标文件
c.后缀为.exe的文件 可执行文件
本文将对这些文件的作用以及由来做出解析。
关于环境
在ANSI C的任何一种实现方式中,均存在两个不同的环境:
a.翻译环境:在此环境中,源代码被转换为可执行的机器指令
b.执行环境:用于实际执行代码
二、翻译环境
由于VS无法直观观察到源文件编译的整个过程,使用Linux中的gcc环境做为演示。
为了方便观察,对以下两个源文件进行操作:
// add.c
int Add(int x ,int y)
{
return x+y;
}
// test.c
#include <stdio.h>
#define a 20
// 声明其他源文件中的函数
extern int Add(int x, int y);
int main()
{
int b = 10;
int c = Add(a, b);
printf("%d\n", c);
return 0;
}
在翻译环境中,执行程序时源文件重要经过两个步骤:
a.编译
b.链接
1.编译
通过编译,源文件生成了目标文件。
但编译的整个过程又可划分为三个阶段:预编译;编译;汇编。
(1)预编译阶段
预编译阶段也成为预处理阶段。
在gcc命令中使用-E选项对源文件进行预处理,使用-o指令将预处理后的内容重定位到test.i中。如下:
gcc test.c -E -o test.i
使用ls -la命令查看当前文件夹内的文件,可见新增一个名为test.i的文件。使用vim编辑器打开此文件。可见如下(包含的头文件过长,仅截取距离main函数较近片段):
与源文件进行对比,可见包含的<stdio.h>变成了大段的代码,这一段代码就是该库函数中的内容。使用#define定义的a所在行也消失了,在main函数中的a则变成了预定义的20。对于extern所在行的注释也被删除了。
对于预处理过程所执行的操作,可总结为三步:
a.头文件的替换;
b.注释的删除;
c.#define 定义的符号的替换。
(2)编译
编译阶段是整个编译过程中最重要的步骤。这个阶段将预处理后的代码翻译为了汇编代码。
在gcc命令中使用-S对预处理后的内容进行编译,同样使用-o输出到指定文件。
gcc test.i -S -o test.s
可见新增了test.s的文件。使用vim打开如下:
对于汇编代码,不做详解。
但在这一步主要进行了以下操作:
a.语法分析;
b.词法分析;
c.语义分析;
d.符号汇总。
对于符号汇总,以test.c做简要解释。
对于test.c,可简要看作main函数+extern声明的Add函数。(以及其他全局变量)
在内存中,对于这些内容,都会有对应的地址。
符号汇总即将这些内容及其地址生成一个此文件对应的表格。
(3)汇编
汇编是编译过程的最后一步。这一步将汇编代码转换为目标文件。
在gcc命令中使用-c选项生成目标文件,-o输出到指定文件。
gcc test.s -c -o test.o
可见新增了test.o的文件。使用vim打开会是如下情况:
这是因为目标文件中存放的是二进制代码。
在这一步的操作中,生成了符号表。
2.链接
链接,即将不同源文件生成的目标文件进行链接,为程序的执行做好准备。
主要有两项内容:
(1)合并段表
(2)符号表的合并与重定位
test.c在编译过程中的符号汇总上已有讲述。add.c所操作的符号汇总与之类似。
但需要注意一点,在test.c中,指向声明的Add函数的地址并不是实际上Add函数的地址,而add.c中的Add函数的定义才是实际上的地址。这一步就是将两个源文件的符号表进行合并,并将其中名称相同的内容所指向的地址修改为实际地址。如test.c中的Add与add.c中的Add进行合并,但是保留add.c中Add的实际地址。
使用gcc对程序执行所需的源文件进行操作,生成可执行文件:
gcc test.c add.c -o test.out
可见生成了test.out的文件。
二、执行环境
在linux环境中,我们使用"./+可执行文件"的格式对程序进行执行。如下:
./test.out
如下:
关于程序的执行过程,主要包含以下几个部分:
1.程序必须载入内存中。有操作系统就由操作系统完成;独立环境必须手动安排。
2.程序开始执行。接着就调用main函数。
3.开始执行程序。在这个过程中,不同类型(全局和局部)会调用不同的内存区块参与执行。静态内存中的变量会在整个过程中一直保存它们的值。
4.终止程序。包括正常的终止main函数;也可能是遇到错误或断电等意外终止。