翻译环境与运行环境
在 ANSI-C 的任意实现中,存在两种不同的环境:
-
翻译环境:在此环境中,源代码被转换为可执行的机器指令(二进制代码)。
-
运行环境:用于实际执行代码的环境。
由于 C 语言代码是以文本形式编写的,计算机无法直接理解,因此必须将这些文本信息翻译成机器指令。
这就好比一位只会中文的中国人和一位只会英语的美国人想要交流,必须借助翻译作为媒介;同样,要想用文本指令控制计算机,也必须借助像
gcc这样的"翻译官"。

翻译环境
那么,翻译环境是如何将源代码转换为可执行的机器指令的呢?实际上,翻译环境包括 编译 和 链接 两大过程,而 编译 又可进一步分解为:预处理 、编译 和 汇编 三个步骤。

在一个 C 语言项目中,可能有多个.c文件共同构建。这些.c文件会先单独经过编译器处理,生成各自对应的目标文件。
随后,这些目标文件与所需的链接库 (如:动态库、静态库)一起,由链接器处理,最终生成可执行程序。(库是支持程序运行的基本函数集合)
补充 :在Windows 环境下的目标文件的后缀是
.obj,Linux 环境下目标文件的后缀是.o
如果进一步展开编译器的三个步骤,整个过程如下所示

1. 预处理(Preprocessing)
预处理阶段主要处理源文件中以 # 开头的预编译指令。在该阶段,源文件和头文件会被处理成以
.i 为后缀的文件。
使用gcc观察预处理后的文件,可执行以下命令:
Shell
gcc -E test.c -o test.i
预处理规则:
-
删除所有
#define并展开所有宏定义。 -
处理所有条件编译指令,如
#if、#ifdef、#elif、#else、#endif。 -
处理
#include指令,将被包含的头文件内容插入到该预编译指令的位置。这个过程是递归进行的,也就是说被包含的头文件也可能包含其他文件。 -
删除所有注释。
-
添加行号和文件名标识,便于后续编译器生成调试信息。
-
保留所有
#pragma指令,供编译器后续使用
预处理后的.i文件不再包含宏定义,因为宏已经被展开。且所有包含的头文件内容已被插入到.i文件中。所以若需确认宏定义或头文件包含是否正确,可查看预处理后的.i文件。
2. 编译(Compilation)
编译过程将预处理后的文件进行词法分析 、语法分析 、语义分析及优化,生成相应的汇编代码文件。
编译命令如下:
Shell
gcc -S test.i -o test.s
以以下代码为例:
Shell
array[index] = (index + 4) * (2 + 6);
词法分析
源代码被输入扫描器,进行词法分析,将代码中的字符分割为一系列记号(如关键字、标识符、字面量、特殊字符等)。上述代码经词法分析后,得到 16 个记号:
| 记号 | 类型 |
|---|---|
| array | 标识符 |
| [ | 左方括号 |
| index | 标识符 |
| ] | 右方括号 |
| = | 赋值 |
| ( | 左圆括号 |
| index | 标识符 |
| + | 加号 |
| 4 | 数字 |
| ) | 右圆括号 |
| * | 乘号 |
| ( | 左圆括号 |
| 2 | 数字 |
| + | 加号 |
| 6 | 数字 |
| ) | 右圆括号 |
语法分析
接下来语法分析器 对扫描产生的记号进行语法分析,从而产生语法树。这些语法树是以表达式为结点的树

语义分析
语义分析器进行语义分析,即对表达式进行静态语义检查,包括声明与类型的匹配、类型转换等。此阶段会报告语法错误。

3. 汇编(Assembly)
汇编器根据汇编指令和机器指令的对照表,将汇编代码转变为机器可执行指令,每个汇编语句几乎都对应一条机器指令。此过程不进行指令优化。
汇编命令如下:
Shell
gcc -c test.c -o test.o
4. 链接(Linking)
链接是一个复杂的过程,涉及地址与空间分配 、符号决议 和重定位等步骤。其主要解决多文件、多模块间的相互调用问题。
例如,一个项目中包含test.c和add.c两个文件:
-
test.c经编译生成test.o -
add.c经编译生成add.o
若test.c中使用了add.c中的Add函数和全局变量g_val,在编译test.c时,编译器并不知道Add 和g_val的实际地址,因此会暂时搁置这些符号的地址。链接时,链接器会在其他模块中查找这些符号的地址,并将test.c中所有引用这些符号的指令重新修正为正确的地址。此过程称为**"重定位"**。
运行环境
程序执行前必须先载入内存:
-
在有操作系统的环境中:通常由操作系统完成载入。
-
在独立环境中:需手动安排载入,或将可执行代码置入只读内存(如:单片机烧录)。
程序执行始于main函数的调用。执行过程中,程序使用:
-
运行时堆栈(Stack):存储局部变量和返回地址。
-
静态内存(Static):存储静态变量,其值在整个程序执行期间保持不变。
程序终止方式包括:
-
正常终止(
main函数返回) -
意外终止
以上简要介绍了 C 程序从编译链接到最终执行的整个过程。若希望深入了解目标文件格式(如:.elf)、链接的底层实现(如:空间分配、符号解析与重定位等),推荐阅读**《程序员的自我修养》**一书。