前言:
在接触编程语言的过程中,我最开始只是会照着葫芦画瓢,而不去思考底层的一些原理:我们写在编译器里面的代码是怎样一步一步转换成我们所需要的结果的,这其中究竟发生了什么?这就与编译链接等相关了。本次主要是分享我初步接触编译、链接的一些学习历程。
ANSI C实现环境:
在ANSI C的任何一种实现中,都存在两种环境:翻译环境和运行环境。
1、翻译环境:在这个环境中,编译器和链接器将源代码转换为可执行代码,可执行代码是机器语言,也就是二进制指令;
2、运行环境:这个环境是用于执行被转换之后的二进制指令的。
在翻译环境中又细分为:编译和链接。
翻译环境:
那么翻译环境是怎样将源代码转换成二进制指令的呢?
翻译环境是由编译的链接两个大过程构成的;编译又分为:预处理、编译、汇编。
一个C语言项目中可能会由多个.c文件构成,那么多个.c文件怎样构成可执行文件呢?
多个.c文件通过编译器处理单独处理之后生成对应的目标文件;在windows环境下目标文件的后缀是.obj ,Linux环境下目标文件的后缀是 .o 。
多个目标文件通过链接器处理之后生成可执行文件;链接库是指运行库(它是支持程序运行的基本函数集合)或者称为第三方库。
如果在把编译器中的过程展开,那么就分为以下的过程:
编译过程:
一、预处理:
在预处理阶段,源文件和头文件会被处理成 .i 为后缀的文件;
在预处理阶段主要处理的是一些#开头的预编译指令,例如#include #define ;
处理的规则:
1、将所有的#define删除,并且展开所有的宏定义;
2、处理所有的条件编译指令,例如 :#if #ifdel #elif #else #endif ;
3、处理#include的预编译指令,将包含的头文件插入到该预编译指令的位置,这个过程是递归进行的,也就是说被包含的头文件可能包含其他的文件;
4、删除所有的注释;
5、添加行号和文件名标识,方便后续编译器生成调试信息;
6、保留所有的#pragma的编译器指令,编译器会后续使用;
预处理之后不在包含任何宏定义,它们都被展开;并且所有的#include指令都被插到了 .i 文件中,所以我们无法知道宏定义或者头文件是否包含正确的时候,我们可以检查预处理之后的 .i 文件来判断。
二、编译:
编译就是将预处理之后的文件进行:词法分析、语法分析、语义分析以及优化,生成相应的汇编代码文件。
1、词法分析:源代码程序被输入扫描器,扫描器把代码中的字符分割成一系列的记号(关键字、标识符、字面量、特殊字符等)
2、语法分析:语法分析器对扫描之后的记号进行分析,从而产生语法树,这些语法树是以表达式为节点的树。
3、语义分析:语义分析器对表达式的语法层面进行分析,编译器能做的分析是语义的静态分析。静态语义分析包括声明和类型的匹配、类型的转换等。这个阶段会报出错误的语法信息。
三、汇编:
编译器将汇编代码转换成机器可以执行的指令,每一条汇编语句几乎都对应着一条机器指令。就是根据汇编指令和机器指令的对照表一一进行翻译,也不做指令优化。
四、链接:
链接是一个复杂的过程,链接时链接器将一堆文件链接在一起生成可执行文件;
链接的主要过程分为:地址和空间分配,符号决议和重定位;
链接解决的是一个项目中多模块、多文件相互调用的问题;
运行环境:
1、程序必须载入内存中。在有操作系统的环境中,一般由操作系统完成;在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2、程序的执行便开始,接着调用main函数。
3、开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),用来存储函数的局部变量和返回地址;程序也可以使用静态(static),存储静态内存中的变量,以在程序的执行过程中一直保留它们的值。
4、终止程序,正常终止main函数;也有可能时异常终止。