第2章
c语言是编译型语言,和python不同(解释型语言)。Linux环境下,可以使用gcc来编译这个程序(示例程序的名字为hello.c)
gcc hello.c
./a.out
在这个编译的过程中可以分成4个步骤:预处理(预编译)、编译、汇编和链接
预编译
对源文件hello.c和相关的头文件进行操作,如stdio.h
等头文件被编译器cpp预编译成一个.i
文件,如果
是一个c++的程序,其源代码的扩展名一般是.cpp
,其预编译后的文件扩展名是.ii
。可以使用如下的指令对程序只进行预编译处理
gcc -E hello.c -o hello.i
#或者可以这样写:cpp hello.c > hello.i
预编译主要处理的是源码中以#
开始的预编译指令,例如#include
、#define
等
预编译过程中的一些步骤
1、展开所有的宏定义
2、删除所有的注释"//"和"/**/"
3、有一个特殊的#pragma
指令需要保留,因为编译器需要使用
显然,第一步预编译结束后所有的宏定义应该都被展开了,换而言之,.i
文件中不包含任何的宏定义,同时源码中包含的文件(包括头文件)也被插入到.i
文件中
可以打开看看这个预编译的文件
编译
把预编译后得文件进行一系列的分析和优化后生产相应的汇编代码文件。编译的过程相当于如下的命令
gcc -S hello.i -o hello.s
当然,现在的版本的GCC把预编译和编译合并成一个步骤,使用一个叫cc1的程序来完成。
cc1是对于c语言的代码而言的,c++对应的是cc1plus,java是jc1。。实际上gcc这个命令是后台的包装,会根据不同的参数调用预编译编译程序cc1、汇编器as、链接器ld。
生成的hello.s文件如下
汇编
将机器代码转换成机器可以执行的指令,就是根据汇编指令和机器指令的对照表一一对应翻译。这个过程可以调用汇编器as执行
as hello.s -o hello.o
#或者
gcc -c hello.s -o hello.o
也可以使用gcc命令从c的源代码文件开始,经过预编译、编译和汇编直接输出目标文件
gcc -c hello.c -o hello.o
链接
这是一个比较有意思的地方,在上一步汇编后已经生成了一个目标文件而不是可执行文件
先看一下目标文件和可执行文件的file类型
显然,从目标文件到可执行文件,这之中需要一个链接的过程
同时可以看到,要将一大堆的文件链接起来才可以得到"a.out",即最终的可执行文件
编译器做了什么
对应的是程序在编译的过程,一般分为6个步骤:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化
从源代码到目标代码,以一段c语言代码为例
array[index]=(index+4)*(2+6)
CompilerExpression.c
词法分析
首先源代码被送到扫描器中,只进行词法的分析,使用一种类似于有限状态机的算法分割字符序列,产生一系列的记号
记号一般有这样的几类:关键字、标识符(array、index)、字面量(包含数字、字符串等)、特殊符号
有一个叫lex的程序可以实现词法扫描的功能,会按照用户已经定好的规则将输入的字符串分割为一个个记号,这样就无需为每个编译器开发一个独立的词法扫描器,而只需要改变语法的规则即可。
语法分析
语法分析器对上面分割好的记号进行语法的分析从而产生语法树。整个分析过程采用的是上下文无关语法的分析手段(虽然我也不懂是啥玩意),而语法树就是以表达式为节点的树
同样的,语法分析也有一个现成的工具叫做yacc。对于不同的语言,编译器的开发者只需要改变语法规则而无需为每个编译器写一个语法分析器,所以又被称为"编译器编译器"。
语义分析
分析完语法,便来到语义分析,编译器所能分析的语义是静态语义,即在编译期间可以确定的语义,与之对应的是动态语义,只有在运行期可以确定的语义。
静态语义通常包括声明和类的匹配、类型的转换。
中间语言的生成
这里是一个源码级优化器,即在源代码级别进行优化。
例如上面的(2+6)可以被优化成8,但是直接在语法树上做优化比较困难,所以在优化的时候常常把语法树转换成中间代码,也是语法树的顺序表示
比较常见的中间代码是三地址代码:x = y op z
中间代码让编译器被分为前端后后端。前端负责产生机器无关的中间代码,后端则将中间代码转换为目标机器代码。
目标代码生成与优化
显然这个过程都是编译器后端,后端主要包括代码生成器、目标代码优化器。前者将中间代码转换为目标代码,后者对前者生成的目标代码进行优化。
模块的拼装-静态链接
一个复杂的软件,人们把每个源代码模块独立地编译,然后按照需求进行"组装"(有点像堆积木),而在这个组装地过程就是链接。链接的主要任务是把各个模块之间相互引用的部分处理好,让各个模块之间能够正确的衔接。链接的主要过程包括了地址和空间的分配、符号决议、重定位
每个源代码文件(.c)经过编译器编译成目标文件(.o),目标文件和库一起链接成可执行文件。其中最常见的库是运行时库,它支持程序运行的基本函数的集合。库是一组文件的包,也就是一些常用代码编译成目标文件后打包存放。(有时候也会把目标文件称作模块)。
在链接的过程中,对其他目标文件中的函数调用的指令必须被重新的调整,这个地址的修正的过程也被叫做重定位