一个软件的开发,一行代码的实现,实际上是由两个环节所构成的:翻译以及运行。
而这两个环节是在两个环境下所进行的:翻译环境和运行环境。
翻译环境负责将源代码转换为可执行的机器指令,也就是计算机能听懂的语言。
运行环境负责实际执行代码的操作。
翻译环境可以被分为两个部分:编译和链接。
而编译又可以被分为三个环节:预处理(预编译)、编译、汇编。
所以整个过程实际上也可以看成是四个环节。
编译
1.预处理(.c文件)
预处理部分顾名思义就是对编译过程进行一个预先的准备工作,预处理后的文件被称为中间文件。它的任务主要包括这几个方面。
(1)头文件包含
预编译会处理源代码中的#include指令,将指定的头文件内容插入到源文件中。这样可以将不同文件中的函数声明、宏定义等内容整合到一个文件中,方便编译器进行后续处理。
(2)宏替换
针对#define定义符号,宏名称,会将其替换为对应的宏定义内容,以达到简化代码编写,提高代码可读性和维护性的目的。
经过预处理后的.i⽂件中不再包含宏定义,因为宏已经被展开。并且包含的头⽂件都被插⼊到.i⽂件 中。所以当我们⽆法知道宏定义或者头⽂件是否包含正确的时候,可以查看预处理后的.i⽂件来确认。
(3)处理条件编译
#开头的指令就是预处理指令,根据条件来判断例如#ifdef、#ifndef、#if等是否编译特定部分的代码。
(4)其他的预处理操作
预处理阶段是为了接下来的编译和汇编过程有一个良好的操作环境而准备的,它能保证后续过程顺利进行。
2.编译
编译会将预处理后的中间文件转换为汇编代码,编译器会进行词法分析、语法分析、语义分析等操作来生成相应的中间表示形式,通常是汇编代码。(.s文件)
(1)词法分析
举例:下面这行代码:
int main() { int a = 10; int b = 20; int sum = a + b; return 0; }
编译器会对其词法进行分析:
标记序列: 1. 标识符(int) 2. 标识符(main) 3. 左括号(() 4. 右括号()) 5. 左大括号({) 6. 标识符(int) 7. 标识符(a) 8. 赋值运算符(=) 9. 常量(10) 10. 分号(;) 11. 标识符(int) 12. 标识符(b) 13. 赋值运算符(=) 14. 常量(20) 15. 分号(;) 16. 标识符(int) 17. 标识符(sum) 18. 赋值运算符(=) 19. 标识符(a) 20. 加号(+) 21. 标识符(b) 22. 分号(;) 23. 标识符(return) 24. 常量(0) 25. 分号(;) 26. 右大括号(})
源代码中的字符序列将被转换为标记序列,而这些标记序列将会在接下来的语法分析中起到作用。
(2)语法分析
语法树的概念:语法树以表达式为节点,这些节点之间带有一定的逻辑关系:
程序 ├─ 声明列表 │ ├─ 声明:int a = 10; │ ├─ 声明:int b = 20; │ └─ 声明:int sum = a + b; └─ 语句列表 ├─ 赋值语句:a = 10; ├─ 赋值语句:b = 20; ├─ 赋值语句:sum = a + b; └─ 返回语句:return 0;
(3)语义分析
再编译过程中,语义分析会对语法分析生成的语法树进行语义检查,以确保源代码的语义是合法的。
针对给定的C语言代码段,可以进行如下的语义分析:
- 变量a、b、sum的声明和使用是合法的。
- 表达式sum = a + b; 中的操作数类型匹配,因为a和b都是整数类型。
- 返回语句return 0; 中的返回值类型与main函数声明的返回类型int匹配。
3.汇编(.o文件(目标文件))
在进行完编译之后,合法的源代码就会进行汇编器进行语言的转换,会将高级语言的代码转换机器代码,也就是二进制指令。
链接
链接过程可以这样理解:链,指的是多个不同的文件;接,指的是将多个文件接在一起,从而生成可执行程序。链接解决的是一个项目中多文件多模块之间互相调用的问题。
其主要功能包括以下几个方面:
(1) 符号解析:在链接阶段,链接器会解析目标文件中的符号(如变量名、函数名等),并将其与其定义所在的目标文件或库文件进行关联。这样可以确保在程序中引用的符号能够正确地找到其定义,从而避免未定义符号或重复定义符号的错误。
(2)符号重定位: 在链接过程中,链接器会根据目标文件中的重定位信息,将各个目标文件中的代码段和数据段进行合并,并调整各个符号在内存中的地址。这样可以确保程序正确地访问和执行各个模块之间的代码和数据。
我们现在有两个源文件:
// main.c extern int global_var; int main() { global_var = 10; return 0; }
// helper.c int global_var;
首先编译时会生成两个目标文件main.o以及helper.o;
链接器会通过符号表来解析和重定位这些符号。在这个例子中,链接器会找到 main.o 中对 global_var 的引用,并将其关联到 helper.o 中 global_var 的定义上。同时,链接器会调整 main.o 和 helper.o 中 global_var 的地址,以确保它们在内存中的位置是正确的。
最终,链接器会将 main.o 和 helper.o 合并为一个可执行文件,并确保 main 函数能够正确地访问和修改 global_var 的值。这样,程序就能够在运行时正常执行,并正确地处理全局变量 global_var。
(3)库文件链接:链接器还会将程序所依赖的库文件链接到可执行文件中。这些库文件包括系统提供的标准库、第三方库或用户自定义库,用于提供各种功能和服务。链接器会将程序中引用的库函数的地址解析并链接到程序中,使得程序能够调用这些库函数。
(4)生成可执行文件:最终,链接器会将各个目标文件和库文件中的代码段和数据段合并,生成一个完整的可执行文件。这个可执行文件包含了程序的所有代码和数据,可以在计算机上直接执行,完成程序的功能。
总的来说,链接部分在编译过程中起着将各个模块整合为一个完整可执行程序的重要作用。链接器将程序的各个部分正确地组合在一起,生成一个可以在计算机上运行的可执行文件。