33.0、C语言——C语言预处理(1) - 翻译环境详解
程序的 翻译环境 和 执行环境
在ANSI C的任何一种实现中,存在两个不同的环境;
第 1 种是翻译环境,在这个环境中源代码被转换为可执行的机器指令;
第 2 种是执行环境,它用于实际执行代码;
详解:翻译环境 = 编译 + 链接
在我们的项目中 可能会出现多个 源文件,那么每一个源文件都会被系统单独的当成一个单元去单独处理;
源文件在经过 编译器 处理之后 会产生一个目标文件也就是 .obj 文件,然后等待每一个目标文件都通过 链接器 和 链接库 处理之后,会将所有目标文件链接到一起,然后产生一个可执行程序 .exe;
- 组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code);
- 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序;
- 链接器同时也会引入标准 C 函数库中任何被该程序所用到的函数,而且他可以搜索程序猿个人的程序库,将其需要的函数也链接到程序中;
那么其实编译又可以细分为三个过程 ->
1. 预编译
2. 编译
3. 汇编
详细介绍:
其实这三个过程在我们的 vs编译器 中不太好体现出来,还是用 Linux 比较容易理解;
预处理阶段 ->
1. 预处理 选项 gcc -E test.c -o test.i 编译完成之后就停下来,预处理之后产生的结果都放在 test.i 文件中;
那么在进行预编译处理的时候,相当于把 #include <xxx.h> 执行了,将所有的头文件包含的内容添加到了我们的 源文件 中;
其实不仅仅只是将头文件包含进了源文件,还做了一些其他的事情,比如说 对源文件的注释删除(使用空格去替换注释),因为注释的信息对编译器来说没有任何意义;
还有比如说 #define 定义的一些常量会在预编译阶段,将这些常量全部替换成 值 ,比如说 #define max 100 那么在预处理阶段会将程序中 所有的 max 换成 100;
其实总的来说,就是一些文本的操作,文本的包含文本的替换啥的;
编译阶段 ->
2. 编译选项 gcc -S test.c 编译完成之后就停下来,结果保存在 test.s 中;
将我们的 C代码 翻译成了 汇编代码,那么详细的的来说就是做了以下一些动作 ->
1. 语法分析
2. 词法分析 ( 涉及到编译原理,这里不细说 )
3. 语义分析
4. 符号汇总 (比如说一些 函数名、全局变量等都汇总起来)
汇编阶段 ->
3. 汇编 gcc -c test.c 汇编完成之后就停下来,结果保存在test.o 中;
将之前编译好的汇编代码,转换成 object 目标文件【 当然这个目标文件我们依然看不懂,因为他是将汇编代码转换成了二进制指令 / 代码 】;然后所有的目标文件通过链接器生产可执行程序;
当然汇编阶段还做了一件事情 -> 形成符号表 ,符号就是之前在编译阶段汇总的符号,那么在汇编阶段会将符号以及符号对应的地址存储到符号表中;
例如像以下代码 ->
这里要汇总的符号有两个 一个是 main 一个是 add;
在该源文件中我们确切的知道 main() 函数式存在的,所以可以有一个相对应的地址存到符号表中;但是 add() 函数只是声明,并不知道他是否确切存在,所以会存入一个无意义的地址;
链接阶段 ->
链接器要做以下两件事请 ->
1. 合并段表;2. 符号表的合并和重定向;
1. 那么先来说一下什么是:合并段表 ->
首先每个目标文件中会分为不同的段,每段会存放一些程序的数据、代码等;不过每个目标文件的段结构、格式是一样的【这种格式被称作 elf 文件格式,感兴趣的可以百度一下】,只是每段存放的东西不一样;
经过链接阶段时,首先会把每个目标文件中相对应的段位置里的数据合并到一起,最终形成一个目标文件;
2. 什么是符号表的合并和重定向 ->
在经过编译阶段后,会形成 符号表,符号表中存放了符号以及他们的地址;那么重复的符号会合并,合并之后如果他们的地址不同则取 有效地址 存入到合并后的符号表中;
符号表的作用是啥呢?
比如说符号表里现在存放着一个函数 Add() 的 符号 Add 以及他的地址,但是这个函数根本就没有定义,只是声明了一下;那当我们在程序中调用这个 Add() 函数的时候,就会去符号表中查找有没有这个符号 Add,查找后发现确实有然后根据他存放的地址去调用Add() 函数,但是发现这个地址是一个无效地址,根本找不到那么就导致函数调用失败了;
当我们的 编译阶段 通过 且 链接阶段 也通过后那么可执行程序就 成功生成了【其实可执行程序也是 elf 文件格式】,那么到这里我们的翻译环境就基本结束了;