lcc源代码详解之概述
2009-03-26 11:06
393 查看
编译器怎样把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.....》一样,因为照常理应该是从词法分析开始,但词法分析出来的单词如何管理呢?这就涉及到符号表,符号表怎样动态增长怎样保存呢?这就涉及到内存管理。同样符号表与类型表有千丝万缕的联系。符号表的符号名字又涉及到字符串的管理。所以自底向上的顺序是:
内存管理---->字符串管理----->符号表管理、类型表管理----->词法分析
源程序:
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.....》一样,因为照常理应该是从词法分析开始,但词法分析出来的单词如何管理呢?这就涉及到符号表,符号表怎样动态增长怎样保存呢?这就涉及到内存管理。同样符号表与类型表有千丝万缕的联系。符号表的符号名字又涉及到字符串的管理。所以自底向上的顺序是:
内存管理---->字符串管理----->符号表管理、类型表管理----->词法分析
相关文章推荐
- lcc源代码详解之介绍
- Java HashMap源代码详解
- 详解python实现FP-TREE进行关联规则挖掘(带有FP树显示功能)附源代码下载(3)
- 详解python实现FP-TREE进行关联规则挖掘(带有FP树显示功能)附源代码下载(5)
- makefile文件详解一--概述
- 实例详解CSS滤镜(1)概述
- spring的事务处理详解:调用一个方法前的事务处理过程(源代码分析)
- android activity详解一:概述
- QQ互动状态使用教程:状态源代码详解(转)
- Linux文件权限详解 文件和目录权限概述
- JAVA集合框架详解(1)--整体概述
- MP4文件格式详解——结构概述
- 详解Android ActionBar之一:ActionBar概述与创建
- 网桥原理及源代码详解
- redis详解(一)-- 概述
- 第一章 计算机、程序和Java概述 课本源代码
- 详解linux系列之linux的概述及安装
- TCP/IP协议详解---概述
- MAC 源代码分析---(一)概述
- Linux下安装PHP,源代码方式安装配置参数详解