读书笔记:程序员的自我修养---第二章

本文详细介绍了编译和链接的过程,包括预编译的宏定义处理、条件编译指令、文件包含、注释删除等步骤;编译阶段的词法分析、语法分析、语义分析、中间代码生成和优化;链接器的工作,如符号决议、重定位,以及静态链接。内容深入浅出,帮助理解编译原理。
摘要由CSDN通过智能技术生成

第二章:编译和链接

预编译

预编译的过程主要处理那些源代码文件中以"#"开始的预编译命令。比如"#include","define"等,主要处理规则如下:

1)将所有的"#define"删除,并且展开所有的宏定义

2)处理所有条件预编译指令,比如"#if","#ifdef","#elif","#else","#endif"

3)处理"#include"预编译指令,将包含的文件插入到该预编译指令的位置。注意,这个过程是递归进程的,就是说被包含的文件可能还包含其他文件

4)删除所有的注释 "//" 和 "/*  */"

5)添加行号和文件标识符,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号

6)保留所有#pragma编译器指令,因为编译器须要使用它们

经过预编译后的.i文件不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经被插入到.i文件中。所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题

编译

编译过程就是把预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生产相应的汇编代码文件,这个过程往往是我们所说的整个程序构建的核心部分,也就是最复杂的部分之一

词法分析

首先源代码程序被输入到扫描器,扫描器的任务很简单,它只是简单地进行词法分析,运用一种类似于有限状态的算法可以很轻松地将源代码的字符序列分割成一系列的记号

词法分析产生的记号一般可分为几类:关键字,标识符,字面量(包括数字,字符串等)和特殊符号(如加号,等号)。在识别记号的同时,扫描器也完成了其他工作。比如将标识符存放到符号表,将数字,字符串常量存放到文字表等,以备后面的步骤使用

宏替换和文件包含等工作一般不归入编译器范围而交给一个独立的预处理器

语法分析

语法分析器将对由扫描器产生的记号进行语法分析,从而产生语法树。整个分析过程采用了上下文无关语法的分析手段。由语法树分析器生成的语法树就是表达式为节点的树,C语言的一个语句是一个表达式,而复杂的语句是很多表达式的组合

数字和符号是最小的表达式,它们不是由其他表达式来组成的,所以它们通常作为整个语法树的节点。在语法分析的同时,很多运算符号的优先级和含义也被确定了下来。如果出现了表达式不合法,比如各种括号不匹配,表达式中缺少操作符等,编译器会报告语法分析阶段的错误

语义分析

语义分析由语义分析器来完成。语法分析仅是完成了对表达式的语法层面的分析,但是它并不了解这个语句是否真正有意思

编译器所能分析的语义是静态语义,所谓静态语义是指编译期可以确定地语义,与之对应的动态语义就是只有运行期才能确定的语义

静态语义通常包括声明和类型的匹配,类型的转换。动态语义一般指在运行期出现的语义相关的问题,比如将0作为除数是一个运行期语义错误

经过语义分析阶段以后,整个语法树的表达式都被标识了类型,如果有些类型需要做隐式转换,语义分析程序会在语法树上插入相应的转换节点,语义分析器对符号表里的符号类型也做了更新

中间语言的生成

源码优化器在不同编译器可能会有不同的定义或有一些其他的差异,源码优化器会在源代码级别进行优化,比如(2+6)这个表达式就可以被优化掉,因为它的值在编译器就能被确定下来

其实直接在语法树上做优化比较困难,所以源代码优化器往往将整个语法树装换成中间代码,它是语法树的顺序表示,其实它已经非常接近目标代码了。但是它一般跟目标机器和运行时环境是无关的,比如它不包含数据的尺寸,变量地址和寄存器的名字等

中间代码的类型有很多种类型,常见的有三地址码,P-代码

三地址码:

x = y op z

这个三地址码表示将变量 y 和 z进行op操作后,赋值给 x。三地址码也得名于此,因为一个三地址码语句里面有三个变量地址

中间代码使得编译器可以分为前端和后端。编译器前端负责产生机器无关的中间代码,编译器后端将中间代码转换成目标机器代码。这样对于一些可以跨平台的编译器而言,它们可以针对不同的平台使用同一个前端和针对不同机器平台的数个后端

目标代码生成与优化

编译器后端主要包括代码生成器和目标代码优化器。代码生成器将中间代码转换成目标机器代码,这个过程十分依赖目标机器,因为不同的机器有着不同的字长,寄存器,整数数据类型和浮点数数据类型等。

目标代码优化器对目标代码进行优化,比如选择合适的寻址方式,使用位移来代替乘法运算,删除多余的指令等

经过扫描,语法分析,语义分析,源代码优化,代码生成和目标代码优化,源代码终于被编译成了目标代码,还有一个问题那就是确定变量的地址

如果变量定义在跟源代码同一个编译单元(同一个文件中),那么编译器可以为其分配空间,确定它们的地址,如果是定义在其他模块的全局变量和函数在最终运行时的绝对地址都要在最终链接的时候才能确定。所以现代的编译器可以将一个源代码文件编程成一个未链接的目标文件,然后由链接器最终将这些目标文件链接起来形成可执行文件

汇编

汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编器的汇编过程相对于编译器来说比较简单,他没有复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了

链接器年龄比编译器长

重定位:重新计算各个目标的地址过程

假设用 "foo" 标记一个子程序

当人们可以使用符号命名子程序或跳转目标以后,不管这个 "foo" 之前插入或减少了多少条指令导致 "foo" 目标地址发生了什么变化,汇编器在每次汇编程序的时候会重新计算 "foo"这个符号的地址,然后把所有用到 "foo" 的指令修正为正确的地址

符号这个概念随着汇编语言的普及及迅速被使用,它用来表示一个地址,这个地址可能是一段子程序(后来发展为函数)的起始地址,也可以是一个变量的起始地址

在一个程序被分割成多个模块后,最常见的属于静态语言C/C++模块之间通信有两种方式,一种是模块间的函数调用,另一种是模块间的变量访问。函数访问须知道目标函数的地址,变量访问也须知道目标变量的地址,这两种方式都可以归结为一种方式,那就是模块间符号的使用

静态链接

人们把每个源代码模块独立地编译,然后按照须要将它们"组装"起来,这个组装的过程就是链接。链接的主要内容就是把各个模块之间相互调用的部分都处理好,使得各个模块之间都能够正确地衔接

链接器的工作就是把一些指令对其他符号地址的引用加以修正。链接过程主要包括了地址和空间分配,符号决议和重定位等这些步骤

符号决议有时候也被叫做符号绑定,名称绑定,名称决议,甚至还有叫地址绑定,指令绑定的,大题意思都一样,但是有一些细节上的区别,"决议"更倾向于静态链接,而"绑定"更倾向于动态链接

目标文件和库一起链接形成可执行文件。最常见的库就是运行时库,它是支持程序运行的基本函数集合。库其实就是一组目标文件的包,就是一些最常用的代码编译成目标文件后打包存放

我们在程序模块main.c中使用另一个模块func.c中的函数foo()。我们在main.c模块中每一处调用foo的时候都必须确切知道foo这个函数的地址,所以它暂时把这些调用foo的指令的目标地址搁置,等待最后链接的时候由链接器去将这些指令的目标地址修正

使用链接器,你可以直接引用其他模块的函数和全局变量而无须知道它们的地址,因为链接器在链接的时候,会根据你所用的符号foo,自动去相应的func.c模块查找foo的地址,然后将main.c模块中所引用到foo的指令重新修正,让他们的目标地址为真正的foo函数的地址

重定位所做的就是给程序中每个这样的绝对地址引用的位置"打补丁",使它们指向正确的地址

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值