ucc源代码阅读(1):主体框架
2014-06-09ucc简介
ucc 是早些年一位清华大学的学生编写的x86平台上的C语言编译器,遵从ANSI C89标准,能在Linux/Windows系统上正确编译自身并成功运行。它有以下特点:
- 代码结构清晰,有详细的文档讲述它的实现(中英文皆有)
- 使用三地址码作为中间码,构建了由基本块组成的控制流图,适合很多优化算法
- 轻量级,编译速度快,词法分析器、语法分析器和目标代码生成器都是自行实现的
在学校学习编译原理课程的时候,在实现C语言词法分析器的时候借鉴过ucc的代码结构,当时ucc的简洁代码结构给我留下了非常深刻的映像,那是我第一次从别人的代码里体会到一种"美感",所以一直都有打算细读一遍其实现,可惜也是一直未曾付诸实践。现在决定每天花点时间,一点一点完成这个目标吧。
嗯,顺便把编译原理的东西复习一遍。
主体框架
ucc.c是编译器的入口模块,其中的main函数如下所示:
int main(int argc, char *argv[]) { int i; if (argc <= 1) { ShowHelp(); exit(0); } Option.oftype = EXE_FILE; SetupToolChain(); Command = Alloc((argc + 60) * sizeof(char *)); Command[0] = NULL; i = ParseCmdLine(--argc, ++argv); for (; i < argc; ++i) { if (argv[i][0] == '-') { Option.linput = ListAppend(Option.linput, argv[i]); } else { AddFile(argv[i]); } } for (i = PP_FILE; i <= Option.oftype; ++i) { if (InvokeProgram(i) != 0) { RemoveFiles(); fprintf(stderr, "ucc invoke command error:"); PrintCommand(); return -1; } } RemoveFiles(); return 0; }
可以很明显地发现,整个main函数可以细分为以下几部分:
- 参数检查
- 环境准备
- 参数解析
- 动作执行
第一部分:参数检查:
if (argc <= 1)
{
ShowHelp();
exit(0);
}
这里只是通过参数个数来进行判断,如果运行时未带参数,则打印帮助信息。
第二部分:环境准备
Option.oftype = EXE_FILE; SetupToolChain(); Command = Alloc((argc + 60) * sizeof(char *)); Command[0] = NULL;
这里的 Option 是一个全局变量,实际是一个结构对象,用来存放ucc编译C语言源代码过程中需要的一些选项,其中一部分是根据外部参数得到的。
SetupToolChain() 用来设置工具链,ucc.c实际上并不是编译器的实际实现,而是一个和操作系统、ucc编译器进行适配的驱动器。ucc并没有实现预处理器、汇编器和链接器,仅实现了词法分析器、语法分析器和目标代码生成器,因此需要操作系统上的工具来做它所未做的事情。在Linux下,这个函数是空的,因为通常Linux系统都有完整的工具链,不需要再额外设置;在Windows下,这个函数会将VS的相关工具作为工具链。
Command 也是一个全局变量,从其名字可以很容易地猜想到它是用来存放要执行的命令的,事实上它也确实是。上面也说到了,在这个主模块中实际上是需要调用其他外部命令的,如系统的预处理器、汇编器和链接器,以及ucc自身的编译器实现ucl。
第三部分:参数解析
i = ParseCmdLine(--argc, ++argv); for (; i < argc; ++i) { if (argv[i][0] == '-') { Option.linput = ListAppend(Option.linput, argv[i]); } else { AddFile(argv[i]); } }
参数解析时,这里并没有通过 getopt 等函数来进行处理,而是逐个处理参数,将其分为选项或者文件名——看上面的代码,一目了然。
第四部分:动作执行
for (i = PP_FILE; i <= Option.oftype; ++i) { if (InvokeProgram(i) != 0) { RemoveFiles(); fprintf(stderr, "ucc invoke command error:"); PrintCommand(); return -1; } }
这个部分的代码我觉得很有意思——主要是用循环来逐步执行编译的各阶段动作,换做是我的话,我可能会写如下的代码结构:
int ret = 0; /* do some thing */ ret = Preproc(); if (ret < 0) { /* error */ return ret; } ret = Lexana(); if (ret < 0) { /* error */ return ret; }; /* ... */
当然,我这里给出的反面示例由于内容较少的缘故,可以很容易地发现可以用循环来简化代码,这正是ucc里做的。在这个循环里,通过循环变量i来控制这个进程,它的初值是 PP_FILE ,这是一个枚举常量,其定义为:
enum { C_FILE, PP_FILE, ASM_FILE, OBJ_FILE, LIB_FILE, EXE_FILE };
这些枚举常量的值,从大到小,正好对应了C语言源程序的完整编译过程中各个阶段的生成结果文件类型,因此通过这个循环就能完成整个流程了,而且进行到哪个阶段也可以简单地通过全局变量 Option 的成员 oftype 来进行控制——这个全局变量的成员的值可以通过执行ucc时的参数来控制。
嗯,今天就到这里啦。