编译器怎样把C源程序翻译成机器代码呢?相信你一定很好奇并想看看具体的例子。好,下面就以一个非常简单例子来说一下编译器的整个工作过程。

源程序:

int round (f) float f; {

              return f+0.5;

}

第一阶段:预处理。

预处理是指宏扩展、引入头文件、选择条件编译代码等工作。其实就是你经常使用的#define、#include<xxx.h>、#ifdef xxx 等语句。预处理程序是作为独立的进程执行的,而且可以混用,比如lcc编译器可以使用GCC的预处理器(这是因为大家都遵循同一个标准ANSI C,所以说标准这东西就是好,一有标准大家就不会乱套)。但对本例而言,经过预处理后源代码还是这个样子,所以这里就不打算深入介绍预处理器了,而且它也不在讨论之内,不会影响对源代码的阅读。

第二阶段:词法分析。

词法分析的任务就是分解出一个个的单词(token)。就像我们英汉翻译一样,你不妨把C源程序当做英语,把机器码当做汉语。好,现在给你一段英文(C源程序),要你把它翻译成中文(机器码)。你想一下你会怎么做?当然你首先要把这一段英文分解成一个个的单词,逐个对照牛津词典(C语言词法规则)。同样,词法分析无非就是把源程序肢解成一个个词法单位,即单词,然后用一张表记下来,等语法分析的时候再拿出来看看。现在这个例子就会被lcc肢解成下面的这张单词表:

单词编码                         附加值

INT                                   inttype

ID                                     "round"

'('

ID                                       "f"

')'                                      

FLOAT                              floattype

ID                                       "f"

';'            

'{'

RETURN        

ID                                         "f"

'+'

FCON                                  0.5

';'

'}'

EOI

其中,EOI代表结束符。附加值给出了单词的更多信息。虽然你现在可能还在对上面表中的某些符号如FCON是什么东西耿耿于怀,其实大可不必,你现在只须对词法分析有一个大致的了解即可,即知道词法分析到底是干什么的就可以了,至于它具体怎么运作,那是日后的分析了。

第三阶段:语法及语义分析。

语法分析就是分析是不是符合C语言的语法要求和规定,关于C语言本身的语法可以参见K&R的经典《The C Programming Language》的附录A,那里对ANSI C进行全面的解释。语义分析就是分析是不是符合语义,比如某些句子可能没有语法错误但有语义错误,比如代码 a = b+3; 使用了未定义的变量a,这就是语义错误。语法分析最终会生成一片森林(数据结构应该学过了吧),森林里有很多树,每棵树都叫做抽象语法树(Abstract Syntax Tree,简称AST)。就本例而言,lcc会生成如下两棵AST:


其中,每个节点的形式是“操作符+类型”。详细如下:

ASGN+F: Assignment + Float 即浮点数赋值运算。

ADDRF+P: Address-Function + Pointer 即指向函数参数的指针,也就是参数的地址。

CVD+F: Convert Double to Float 即将Double转换为Float,同理CVD+I 就是Double转换为Int 以此类推。

INDIR+D: Indirection Double 即取值,值的类型为double。其中INDIR代表取值操作,+号后面代表值的类型。如INDIR+F就是取浮点值。

       第一个AST应该从右下角的" caller "f" ---> double " 逆着箭头并逆时针看。总的流程是这样:从调用者(caller)的f处取值,此值是 double类型,将其转换成float类型并赋值给函数round(被调用者,callee)的参数f。

       第二个AST应该从左下角的" callee "f" ---> float " 逆着箭头并顺时针看。总的流程是这样:从参数f处取值,并转换成double,然后与double类型的常量0.5进行双精度的加运算。运算完后将结果转换成 int型并返回。

       看到这里是不是把源代码给忘了?回过头看看开头处的源代码,返回值是 int 类型没错,参数类型是 float 类型,常量0.5默认是 double 类型。所以一系列的类型强转就在程序员不知不觉的情况下发生了:先是把double类型的实参(假设的)转换成 float 类型的传给参数,由于0.5是double类型的,不得已又再次转换成double类型的,然后才进行运算,最后看看返回类型,不好,是int类型,无奈之下又强转为int类型,最终把结果返回给调用者。

        所以说现在使用高级语言编程的程序员真是幸福啊!随随便便写下像这样的代码:

       int a = 5;   double b = 2.34; char c = a + b;

还没事。因为C编译器已经默默无闻地忍受了这一切!这也是使用高级语言的好处之一啊。但是机器始终是机器,你要告诉它是什么类型的运算它才能算。学过汇编的都知道,就加法而言,就有整型加,浮点型加等等。编译器的很重要的任务之一就是隐藏这些细节,使编程简单。

第四阶段:中间代码的产生。

中间代码阶段将上面的AST转换成DAG(Directed Acyclic graph,无循环有向图)。如下图:


这张图与之前的区别在于将形如 ASGN+F 的改成 ASGNF 以示区别。其中CNST+D变成引用标号为2的静态变量。标号1表示round的结尾。

第五阶段:汇编代码的产生。

你可以看到,DAG已经可以很形象地描述执行代码了。lcc的代码生成器通过对DAG加注释的方法生成汇编代码。注释结果如下图:


每个函数的入口和出口都是一样的汇编代码,所以后端就会准备好一张代码模板,我们只要把生成的代码插进合适的位置就可以了。就X86和DOS/WinNT而言,lcc与MASM或TASM汇编器协同合作,将产生的汇编代码连接成在特定的体系结构和操作系统上的机器码。

 

关于汇编的知识自己掌握,这里不讨论了,以免陷入不必要的细节。

小结:

          从C源程序到汇编代码,我们很快地走了一遍,了解了lcc的工作过程。下一节就进入正题,进行代码注释。注释的顺序与作者著作《A retargetable.....》一样,因为照常理应该是从词法分析开始,但词法分析出来的单词如何管理呢?这就涉及到符号表,符号表怎样动态增长怎样保存呢?这就涉及到内存管理。同样符号表与类型表有千丝万缕的联系。符号表的符号名字又涉及到字符串的管理。所以自底向上的顺序是:

内存管理---->字符串管理----->符号表管理、类型表管理----->词法分析。