编译和链接
一 , 翻译环境和运行环境
在ANSI 的任何一种实现中 , 存在两个不同的环境。
VS2022 --- IDE (集成开发环境) --> 编辑器 + 编译器(cl.exe) + 链接器(link.exe) + 调试器
1. 翻译环境 : 将这个环境中的源代码 --> 可执行的机器指令(二进制指令)
2. 执行环境 :它用于实际执行代码
二 , 翻译环境
翻译环境是由 编译 和 链接 两大过程组成的 。 编译又可以分解成 : 预处理(预编译) , 编译 , 汇编。
- 多个 .c 文件(xx.c) 单独经过编译器 , 编译处理生成对应的目标文件 (xx.obj)
- 在 windows 环境下的目标文件的后缀是 .obj , Linux 环境下目标文件的后缀是 .o
- 多个目标文件和链接库一起经过链接器处理生成最终的可执行程序
- 链接库是指 运行时库 (它是值称程序运行的进本函数集合) 或者第三方库。
以gcc为例 , 拆解编译链接的过程
2.1 预处理(预编译)
在预处理阶段 : 源文件 , 头文件会被处理成 .i 为后缀的文件
在gcc环境下 , 可以观察test .c 文件预处理后的 .i 文件 , 命令如下:
gcc -E test.c -o test.i
预处理阶段主要处理源文件中 #开始的预编译指令 。 比如 #include , #define ,规则如下
- 将所有 #define 删除 , 并展开所有宏定义
- 处理所有条件的编译指令 , 如#if , #ifdef ,#elif , #else , #endif
- 处理#include 预编译指令 , 将包含的头文件内容插入到该预编译的位置 。这个过程是递归进行的 , 也就是说被包含的头文件 , 也可能包含其他文件
- 删除所有注释
- 添加行号和文件名标识 , 方便后续编译器生成调试信息
- 保留所有#pragma 的编译器指令 , 编译器后续会使用
预处理后 :
1. .i 文件不再包含宏定义 --> 都展开了
2. 头文件被插入到 .i 文件中
3. 不知道宏定义 或者 头文件是否包含正确 --> 看 .i 文件
2.2 编译
编译过程就是将预处理后的文件进行一系列的 : 词法分析 , 语法分析 , 语义分析 及 优化 , 生成相应的汇编代码文件。
在gcc 里的编译过程命令如下 :
gcc -S test.i -o test.s
例如:用一下的代码进行编译
array [index] = (index + 4 ) * (2 + 6 )
2.2.1 词法分析
将源代码程序扫入扫描器 ----> 词法分析 ( 把代码中的字符分割成一系列的记号 -- 关键字,标识符,特殊字符等)
2.2.2 语法分析
语法分析器 --> 扫描后的记号进行语法分析 --> 语法树:一表达式为节点的树
2.2.3 语义分析
语义分析器 --> 对表达式的语法层面分析 。
编译器所能做的分析是语义的静态分析 。 静态分析通常包括声明和类型的匹配 , 类型的转换。这个阶段会报告错误的语法信息。
2.3 汇编
汇编器是将或编代码转变成机器可执行的指令 , 每一个汇编语句几乎对应一条机器指令 。 即使更具汇编指令和机器指令的对照表一一的进行翻译 , 不做指令优化。
gcc -c test.s -o test.o
2.4 链接
链接是一个复杂的过程 , 链接的时候需要把一堆文件链接在一起 ,才可生成可执行程序
链接过程包括 : 地址和空间分配(主要是对全局变量和函数来说) , 符号决议 和 重定位等的这些步骤。
链接解决的是一个项目中多文件 , 多模块之间互相调用的问题 。
比方说 C 项目中有两个 .c 文件 -- Add.c / test .c
//test.c
#include <stdio.h>
extern int Add(int x, int y);
extern int g_val;
int main()
{int a = 2;int b = 3;int c = Add(a, b);printf("%d\n", c);printf("g_val = %d\n", g_val);return 0;
}
//Add.c
#define _CRT_SECURE_NO_WARNINGS 1
int g_val = 2022;int Add(int x, int y)
{return x + y;
}
源文件单独经过编译器生成对应的目标文件:
test.c 经过编译器处理生成 test.o
Add.c 经过编译器处理生成Add.c
1. test.c 用了Add.c 文件中的Add 函数 与 g_val 变量
2. 在test .c 中每次用Add函数 与 g_val 变量的时候 必须得确切知道地址 ,每个文件单独编译 , 所以test .c 编译时 --> 不知道它两地址 , 所以把它两地址搁置
3.最后链接时候 , 链接器根据引用的符号 , 在其他模块中找 地址 , 然后把 地址搁置 处的地址重新修正 --> 这个过程也叫:重定位
这只是很简洁的讲解了C程序如何编译 与 链接 ,最总生成可执行程序的过程 ,很多内部细节无法展开详解 , 推荐看 《程序员的自我修养》 这本书来了解!
三 ,运行环境
1. 程序必须载入内存中 。 在有操作系统的环境中 : 一般这个由操作系统完成 。 在独立环境中,程序的载入必须由手工安排 , 也可能是通过可执行代码置入只读内存来完成 。
2. 程序的执行便开始 , 接着调用main 函数
3.开始执行程序代码 。 这时程序将使用一个运行时堆栈 (stack) ,存储函数的局部变量和返回地址。程序同时也可以使用静态(static) 内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值
4.程序终止 。 正常终止main函数;也可能是意外终止。