C/C++程序的声明周期(详解)

目录

1 预编译

2 编译

2.1 扫描(词法分析)

2.2 语法分析

2.3 语义分析

2.4 中间语言生成

2.5 目标代码生成与优化

3 汇编

4 链接

 


一个程序从编辑完成到执行一共包括四个部分:预编译、编译、汇编、链接,那么每一个部分具体做了哪些工作呢?接下来将对这四个部分进行详细的介绍。

1 预编译

预编译是程序声明周期的第一个环节,在预编译阶段,源代码文件(.c)和相关的头文件会被预编译器预编译成一个.i文件(.cpp文件会被预编译成.ii文件),Linux下的预编译命令为:

gcc -E hello.c -o hello.i

预编译过程主要是处理以“#”开头的预编译指令,如“# include”和"# define"等:具体工作如下所示:

  • 将所有的”# define”删除,并且展开所有的宏定义;
  • 处理所有的条件预编译指令。如:#if、#ifdef、#elif、#else、#endif;
  • 处理“# include”预编译指令,将被包含的文件插入到该预编译指定的位置,这个过程是递归的,被包含的文件可能还包含其他文件;
  • 删除所有的注释;
  • 添加行号和文件标识名,如:#2 “hello.c” 2,以便编译时编译器产生调试用的行号信息以及用于编译时产生的编译错误和警告时能够显示行号;
  • 保留所有的#pragma编译器指令,因为编译器必须要使用它们

当生成.i文件之后,可以直接查看.i文件,会发现.i文件里面除了当初我们自己些的程序,还有许多外部的文件被包含进去了,最简单的hello.c文件,通过查看点.i文件,会发现,main函数里面的内容,出现在了.i文件的最末端,前面的都是stdio.h文件以及stdio.h所包含文件的内容。

 

2 编译

 编译过程就是把预编译完的文件进行一系列的词法分析、语法分析、语义分析以及优化后产生相应的汇编代码文件,编译过程是程序的整个声明周期的核心部分。可以使用如下Linux指令完成程序的编译过程。

gcc -S hello.i -o hello.s

虽然编译的指令很简单,但是编译过程是一个很复杂的过程。编译过程一般可以分为6部:扫描、语法分析、语义分析、源代码优化、目标代码代码生成和优化。

2.1 扫描(词法分析)

首先,源代码会被输入到扫描器,进行简单的词法分析,运用一种类似于有限状态机(学过《形式语言与自动机》应该很了解)的算法可以将源码分解成一系列的记号。产生的记号一般可以分为如下几类:关键字、标识符、字面值和特殊符号(如加号、减号)。

例如:如下代码的扫描

str[index] = str[index-1]+4

扫描结果:

在识别记号的同时,扫描器也完成了其他工作。比如将标识符放到符号表、将数字、字符串常量存放到文字表等,以备后面的步骤使用。

2.2 语法分析

接下来语法分析器将扫描器产生的记号进行语法分析,从而产生语法树。语法树是通过按照上下文无关语法进行推导所形成的树。下面看一个语法树的例子。

array[index]=(index+4)*(2+6)

上述表达式产生的记号表后,进行语法分析产生的语法树如下图所示:

从图中可以看出,整个语句被看成是一个赋值表达式;赋值表达式左边是一个数组表达式,右边是一个乘法表达式;数组表达式又由连个符号的表达式来组成。在语法分析的同时,很多运算符号的优先级也被确定下来了。另外有些符号具有多重含义,比如星号*在c语言中可以表示乘法,也可以表示对指针取内容,所以语法分析阶段必须对这些内容进行区分。如果出现了表达式不合法,比如各种括号不匹配,表达式缺少操作符等等,编译器就会报告语法分析阶段的错误。

2.3 语义分析

语义分析由语义分析器来完成。语法分析仅仅是对表达式的语法层面的分析,但是它并不了解这个语句是否真正有意义。比如在C语言里面两个指针做乘法运算是没有意义的,但是这个语句在语法上是合法的。编译器所能分析的语义是静态语义,即在编译器可以确定地语义,与之对应地是动态语义,即在运行期才能确定的语义。

静态语义通常包括声明和类型的匹配,类型的转换。比如当一个浮点型的表达式赋值给一个整型的表达式时,其中隐含了一个浮点型到整型的转换过程,语义分析需要完成这个步骤。比如当一个浮点型赋值给一个指针的时候,语义分析器会发现这个类型不匹配,编译器就会报错。

动态语义一般指在运行期出现的语义的相关问题,比如将0作为除数是一个运行期的语义错误。

经过语义分析阶段以后,所有的表达式都被标识了类型,如果有些类型需要做隐式转换,语义分析程序就会在语法树中插入相应的转换节点。上面的语法树经过语义分析后成为如下图所示的形式:

同时,语义分析器还对符号表里面的符号类型做了相应的更新。

2.4 中间语言生成

在这一部分,源码级优化器会在源代码级别进行优化,在上述的语法树中,(2+6)这个表达式是可以被优化掉的,因为它的值在编译期就可以确定下来。类似的还有很多复杂的优化过程。由于直接在语法树上做优化比较困难,所以源码级优化器往往会将整个语法树转化为中间代码,它是语法树的顺序表示,已经很接近目标代码了,但它一般跟目标机器和运行时的环境是无关的,比如它不包含数据的尺寸。变量地址和寄存器的名字等。

中间代码有很多类型,比较常见的有三地址码P-代码。拿三地址码来说,它是这样子的:

x = y op z

在上述例子中,语法树可以被翻译成如下所示的三地址码:

t1 = 2 + 6
t2 = index + 4
t3 = t1 * t2
arry[index] = t3

这里有三个临时变量:t1、t2和t3。在对三地址码进行优化时,优化器程序会将2+6的结果计算出来,得到t1=8,然后将后面表达式中所有的t1都替换成8,还可以省掉临时变量t3,因为t2可以反复利用,经过优化后的三地址码如下所示:

t2=index+4
t2=t2*8
array[index] = t2*8

中间代码的生成使得编译器分为了前端和后端,前端复杂产生机器无关的中间代码,后端则将中间代码转化为机器代码(汇编代码)。

2.5 目标代码生成与优化

代码生成器会将中间代码转化为机器代码,这个过程非常依赖机器,因为不同的机器有着不同的字长、寄存器、整数数据类型和浮点数据类型等。对于上述产生的中间代码生成的机器代码可能如下所示:

最后目标代码优化器会对上述的机器代码进行优化,比如选择何时的寻址方式、使用位移来代替乘法运算、删除多余的指令等。

 

经过这些扫描、语法分析、语义分析、中间代码生成、代码生成和目标代码优化,源码会被编译成目标代码(机器代码)。

3 汇编

汇编器是将汇编代码(机器代码)转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编器的汇编过程相对于编译器的编译过程来说时非常简单的,只需要根据汇编指令和机器指令的对照表一一翻译就可以了。

汇编可以使用如下操作来完成:

gcc -c hello.s -o hello.o
//或者
gcc -c hello.c-o hello.o

4 链接

链接可以通过如下linux命令来完成:

gcc -o hello hello.o

链接包含的内容比较杂,包含文档结构、可重定位文件等。可以参照如下链接的内容进行了解:

链接详细介绍

 

 

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值