前言
程序员的日常工作离不开程序环境和预处理,本文将为您详细解析它们的内部机制和运作原理。
程序的翻译环境和执行环境
在ANSI C(标准C)的任何一种实现中,存在两个不同的环境。
- 翻译环境,在这个环境中源代码被转换为可执行的机器指令
- 执行环境,它用于实际执行代码
什么意思呢?
计算机只能执行二进制指令,我们写的C语言程序属于文本信息,计算机不能直接理解
翻译环境:在这个环境中就是将C语言代码翻译成二进制指令,这些指令会放在可执行程序当作。
执行环境:当我们得到可执行程序时,如何让它运行起来呢?这时就需要到执行环境,执行环境就是用来执行二进制的代码。
翻译环境的整体流程如下:
我们写的每一个.c文件都属于源文件。一个项目当中可以包含多个.c文件。在翻译环境中,每个源文件会单独的经过编译器处理,生成目标文件(.obj文件),每个目标文件与链接器捆绑在一起,形成一个单一而完整的可执行程序。
那链接器的作用是什么呢?
链接器会引入标准C函数库中被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
翻译环境
在翻译环境下,计算机会进行两大操作,一个是编译,一个是链接。
编译又可分为3个阶段:
在开始编译后,会先进入预编译阶段,预编译阶段会做什么呢?这些我们在VS这种集成开发环境中是看不到的。有使用vscode的小伙伴可以试一试。
例如我们写一个简单的程序并进行预处理,如下图:
输入指令让程序先进行预处理,预处理后我们可以看到在我们写的代码前多了800多行代码,这800多行代码是怎么来的呢?其实这增加的代码是我们包含的头文件s t d i o . h里的内容。
那在预处理阶段,计算机都对程序做了什么呢?主要做了一下三个操作:
- 注释的删除
- #include 头文件的包含
- #define 符号的替换
这些都属于文本操作,所有的预处理指令都是在预处理阶段处理的。
如下图,左边为翻译后的汇编指令。
预处理之后,程序就会进入编译阶段,编译阶段会将我们写的C语言代码翻译成汇编指令,它主要会对C程序进行一下操作:
- 语法分析
- 词法分析
- 语义分析
- 符号汇总
编译之后程序进入汇编阶段,汇编阶段就会生成目标文件。目标文件中存放的都是二进制指令。
所以在汇编阶段,计算机主要干了一件事:把汇编代码,翻译成机器可以读懂的二进制指令。
目标文件一般是不打开的,如果要强制打开,看到的也只是一些乱码。
在汇编阶段其实有一个动作叫:形成符号表
在编译阶段程序会进行符号汇总,汇编阶段又会进行形成符号表的操作。这有什么用呢?
其实它们最终的用途是在链接阶段。在链接阶段,就会去查看这个符号表
在链接阶段会进行一下两个操作:
- 合并段表
- 符号表的合并和符号表的重定位
这两个操作主要干什么呢?
我们可以先写一个简单的程序来观察:
在我们生成目标文件时,我们可以打开生成的这个二进制文件,打开文件之后,虽然说大部分东西都看懂,但是我们可以找到一个 E L F
这种二进制文件看似乱码,实则具有自己的组织格式。在Linux环境下都是使用ELF这种组织格式来存储的。我们以Linux环境下为例。
核心就是,像elf这种组织格式,它的存储方式其实是将数据划分为一个一个的段,按照不同的段来存储(数据段,文本段,只读数据段等)。
也可以输入指令去读这个二进制文件中的符号,打开我们可以看到一个个的全局符号
在编译阶段,就已经进行了这些全局符号的汇总。
我们分两个文件去写程序:
test.c
1. extern int Add(int, int); 2. int main() 3. { 4. int a = 10; 5. int b = 20; 6. int c = Add(a, b); 7. printf("%d", c); 8. }
add.c
1. int Add(int x, int y) 2. { 3. return x + y; 4. }
在编译时就会对两个文件中的符号进行汇总,add.c里边汇总一个符号Add,test.c文件汇总两个符号Add和main。
编译后进入汇编阶段,汇编阶段形成符号表,符号表里边存放的是汇总的符号,以及相应的地址。
这里回到链接阶段的两个操作:
合并段表,我们前边知道在汇编阶段,生成的二进制文件,数据存储是分段存储的。上述我们使用两个文件来写程序,两个文件都是这样分段存储的。
到了链接阶段,两个文件合并对应位置上的数据段,并生产.exe结尾的可执行程序,它也是符合elf这样的分段存储。
还有符号表的合并,在add.c中汇总一个符号Add,有函数定义为有效地址,test.c中Add仅仅是一个声明,所以并不是一个有效地址,两个文件的符号表也会在链接阶段进行合并,生成一个新的符号表
这有什么用呢?在程序运行时,就会通过符号表上的地址去找到对应的函数。
这整个过程就是程序在编译、链接生成可执行程序的整个过程。
运行环境
程序执行的过程:
- 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
- 程序的执行便开始。接着便调用main函数。
- 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
- 终止程序。正常终止main函数;也有可能是意外终止