ZMonster's Blog 巧者劳而智者忧,无能者无所求,饱食而遨游,泛若不系之舟

ucc源代码阅读(1):主体框架

ucc简介

ucc 是早些年一位清华大学的学生编写的x86平台上的C语言编译器,遵从ANSI C89标准,能在Linux/Windows系统上正确编译自身并成功运行。它有以下特点:

  1. 代码结构清晰,有详细的文档讲述它的实现(中英文皆有)
  2. 使用三地址码作为中间码,构建了由基本块组成的控制流图,适合很多优化算法
  3. 轻量级,编译速度快,词法分析器、语法分析器和目标代码生成器都是自行实现的

在学校学习编译原理课程的时候,在实现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时的参数来控制。

嗯,今天就到这里啦。