1.4.1 UCC的使用
通过第1.3节的例子ucc\examples\sc,我们对如何根据语言的文法来编写语法分析器,建立语法树,之后在语法树的基础上生成中间代码有了一个感性的直观认识。当然,光有这些中间代码,C程序员还不能得到其所要的计算结果。C编译器还要由中间代码产生汇编代码。在剖析UCC编译器前,让我们先熟悉一下UCC编译器的使用。UCC编译器的大部分代码都是用标准C语言并调用C标准库来编写,可在Linux或Windows系统上生成32位的x86汇编代码,这些代码需要32位的函数库的支持才能在相应系统中运行。在后续的章节中,为节省篇幅,我们主要以32位Ubuntu系统为例来讨论。在VMware等虚拟机上安装32位Ubuntu系统不算太复杂,这里不再画蛇添足。需要在ucc/makefile第1行和ucc/driver/linux.c第7行配置UCC的安装目录UCCDIR,比如” /home/iron/bin”。如果ucc的源代码被解压到” /home/iron/src/ucc”目录,则经过以下步骤就可构建并安装UCC到” /home/iron/bin”中。
iron@ubuntu:ucc$ pwd
/home/iron/src/ucc
iron@ubuntu:ucc$ make -s
iron@ubuntu:ucc$ make -s install
iron@ubuntu:ucc$ make -s test
为了使用户能方便地使用ucc命令,我们还需要设置一下环境变量PATH,具体的操作如下所示,由” cd ~ ”进入当前用户主目录,在gedit打开的.bashrc文件末尾添加一行”exportPATH=$PATH:/home/iron/bin”, 其中”/home/iron/bin”即为前文所设定的UCCDIR,保存后退出,重新打开一个终端,即可使用ucc命令。
iron@ubuntu:ucc$ cd ~
iron@ubuntu:~$ gedit .bashrc
接下来,我们用一个简单的例子来解释一下UCC的大致工作流程。编写以下C代码,存为文件”hello.c”。这份代码用于求阶乘,其中有if语句、while语句、库函数调用及递归函数。
#include <stdio.h>
int f(int n){
if(n < 1){
return 1;
}else{
return n *f(n-1);
}
}
int main(){
int i = 1;
while(i <= 10){
printf("f(%d)= %d\n",i,f(i));
i++;
}
return 0;
}
C源代码hello.c需要先经过预处理器(C PreProcessor)预处理。预处理器会根据预设的include目录去查找并包含头文件,并对宏定义进行展开,如果找不到对应的头文件,则报错,这类错误是预处理报的错,还未到编译器阶段。
预处理器后的结果hello.i,才是作为编译器的输入,诚如UCC的原作者在”ucc\doc\UCC User Manual.txt”中所言” Before reporting bugs: ……. , send the prepocessed files towenjunw@yahoo.cn” ,编译器看到的是preprocessed后的文件。有时侯,编译器可能报出数以百计的语法错误,其原因可能仅是宏定义时出了点差错,这时打开预处理后的文件看看,就能很清楚哪里出问题了。
编译器会对hello.i进行词法分析、语法分析、语义检查和中间代码生成,经过前面几节的准备,我们对这些概念应有点感觉了,这几个阶段被称为编译器的前端,它们与具体的机器无关。C编译器再根据中间代码生成不同硬件平台的汇编语言,这部分工作被称为编译器的后端,与具体的机器相关,因为不同机器的机器指令是各不相同的。当然,编译器还有“优化”这样的重点戏需要完成,这也是编译相关研究的当前热点。而中间代码实际上起到了连结前端和后端的桥梁作用。
有了汇编代码hello.s后,还需要借助汇编器assembler根据汇编语言来“装配”成机器码,由此产生了目标代码hello.o,其中为.o代表的是object,译为“目标”,与面向对象的object是同一个英文单词。既然不同硬件平台的机器代码是不同的,那能不能定义一套中间代码,我们假设有一个虚拟的机器,其机器代码正好就是我们的中间代码。这样,程序员所编写的高级语言被编译成中间代码,再把这些中间代码送给用软件实现的虚拟机来解释执行,各个平台上预先写好各自的中间代码虚拟机,那生成的中间代码就可以跨平台了。这一定让你想起了Java和” Write Once , RunAnywhere”那让人热血沸腾的Slogan。当然,与运行平台相关的工作及优化的重头戏就交给了Java 虚拟机。”天之道损有余 而补不足”,所以总体而言,上帝是公平的。正如在星际争霸里的人族、神族和虫族一样,如果参数太失衡,就不好玩了。Java得到跨平台的代价是牺牲了一部分的运行效率,但在程序员比CPU和内存贵的今天,这种牺牲还是有经济上的意义的。生成中间代码之后解释执行,比”生成机器代码之后直接运行速度更低”的原因,就好比有一本英语原版书,翻译的方法可以有口译和笔译,口译的方式每次都要找口译员帮你解释一下,讲完你可能就忘了;而笔译的好处是笔译员翻译一遍后,你就有了一份中文版的书,以后就不用再麻烦笔译员了。Java虚拟机采取的加速方案有即时编译Just InTime,如果运行时发现有些中间代码要被多次解释执行,那我们干脆就在动态运行时,把相应的中间代码翻译成机器代码吧,这就是所谓的”即时”。当然,道理很简单,做起来很难。
目标模块hello.o还需要携手函数库和其他的目标代码才能得到可执行程序hello,这个工作被称为Linking,即连接,也有写为链接的,哪个是错误字,已经傻傻分不清楚了,权当IT世界的通假字吧,这个工作由连接器Linker完成。在此阶段出现的错误有,全局变量(或函数)重复定义,全局变量(或函数)未定义等。