目录
- 1.翻译环境和运行环境
- 2.翻译环境
- 2.1 预处理(预编译)
- 2.2 编译
- 2.3 汇编
- 2.4 链接
- 3.运行环境
1.翻译环境和运行环境
在ANSI C的任何一种实现中,存在两个不同的环境。
- 翻译环境:在这个环境中源代码被转换为可执⾏的机器指令(⼆进制指令)。
- 执行环境:⽤于实际执⾏代码。
2.翻译环境
翻译环境是怎么将源代码转换为可执⾏的机器指令的呢?本篇文章将讲解一下翻译环境在这个过程中对源代码做的事。
翻译环境由两个大的过程组成:
- 编译
- 链接
而其中编译过程又可分为:预处理(预编译)、编译、汇编三个过程。
-
一个c语言的项目中可能有多个源文件(
.c
文件 ),那它们会如何生成可执行程序(.exe
文件)呢? -
首先它们会单独经过编译器处理生成对应的目标文件。(如
test.c
->test.obj
)- 注:在Windows环境下的⽬标⽂件的后缀是
.obj
,Linux环境下⽬标⽂件的后缀是.o
。
- 注:在Windows环境下的⽬标⽂件的后缀是
-
多个⽬标⽂件和链接库⼀起经过链接器处理⽣成最终的可执⾏程序。
- 链接库是指运⾏时库(它是⽀持程序运⾏的基本函数集合)或者第三⽅库。(库文件的使用需要链接库)
-
如果将编译拆分成三部分,那就变成以下过程:
2.1 预处理(预编译)
预处理阶段,源⽂件和头⽂件会被处理成为.i
为后缀的⽂件。
处理规则如下:
-
将所有的
#define
删除,并展开所有的宏定义。 -
处理所有的条件编译指令,如:
#if、#ifdef、#elif、#else、#endif
。 -
处理
#include
预编译指令,将包含的头⽂件的内容插⼊到该预编译指令的位置(直接将头文件内容全部拷贝过来,并删除#include
这一行)。这个过程是递归进⾏的,也就是说被包含的头⽂件也可能包含其他⽂件。 -
删除所有的注释
-
添加⾏号和⽂件名标识,⽅便后续编译器⽣成调试信息等。
-
或保留所有的
#pragma
的编译器指令,编译器后续会使⽤。
2.2 编译
编译阶段,会生成由汇编语言构成的汇编文件( .s
文件)
生成汇编文件的过程中会进行如下处理:
- 语法分析
- 词法分析
- 语义分析
- 符号汇总(将全局变量名或者函数名汇总,包括
main
,局部变量因为在编译过程还没有创建,只有运行时才创建,所以不汇总)
2.3 汇编
汇编阶段,会将汇编代码处理成机器指令(二进制指令),生成 .o
文件( Linux
环境下)或者 .obj
文件( Windows
环境下)。
同时还会形成符号表,符号表就是将之前汇总的符号与一个地址关联起来。
对一个函数名:
- 如果文件中定义了该函数,则关联一个有效的地址(可以找到该函数并使用)
- 如果文件中只声明了该函数,则关联一个无效的地址(给什么样的无效地址取决于编译器)。
2.4 链接
链接是将多个源文件和链接库一起经过链接器处理生成可执行文件的过程。
该阶段主要进行:
- 段表合并
- 符号表的合并和符号表的重定位
段表合并:经过汇编过程,生成的 .o
文件,如 test.o
、 add.o
等,都有具体的格式:elf
格式,格式将文件划分为不同的区域,存放不同的内容,所以将多个源文件合并生成可执行程序时,需要将相同的区域的内容合并到一起,这一过程称为合并段表。
符号表的合并和重定位:汇编阶段每个源文件都形成了对应的符号表,在链接阶段需要将它们合并起来。下面举一个例子:
例如:在一个c语言项目中有 add.c
(只定义了 函数 add
)和 test.c
(声明了但没有定义函数 add
,且有主函数 main
)。
add.o
中的符号表:
add -> 0x11111111 //(假设的有效地址)
test.o
中的符号表:
add -> 0x00000000 //(假设的无效地址)
main -> 0x22222222 // (假设的有效地址)
因为 test.o
中和 add.o
中都有 add
,但是 add.o
中的地址是无效的,所以合并后取有效地址,这个过程叫符号表的重定位:
add -> 0x11111111 //(假设的有效地址)
main -> 0x22222222 // (假设的有效地址)
有了符号表程序就可以找到对应的函数,如果合并之后某个符号的地址仍是无效的,则程序无法找到这个符号的定义并报错。
add -> 0x00000000 //(假设的无效地址)
main -> 0x22222222 // (假设的有效地址)
合并后这样的符号表程序就会报错。
3.运行环境
-
程序必须载⼊内存中。在有操作系统的环境中:⼀般这个由操作系统完成。在独⽴的环境中,程序的载⼊必须由⼿⼯安排,也可能是通过可执⾏代码置⼊只读内存来完成。
-
程序的执⾏便开始。接着便调⽤
main
函数。 -
开始执⾏程序代码。这个时候程序将使⽤⼀个运⾏时堆栈(
stack
),存储函数的局部变量和返回地址。程序同时也可以使⽤静态(static
)内存,存储于静态内存中的变量在程序的整个执⾏过程⼀直保留他们的值。 -
终⽌程序。正常终⽌
main
函数;也有可能是意外终⽌。