2.1  从Makefile走起

    目录ucc\ucl包含了UCC编译器的所有源代码,而ucc\driver则是UCC驱动的源代码,我们在第1章时已简要地分析过UCC驱动。从第2章开始,我们要剖析的代码就集中在目录ucc\ucl中,ucl就是UCC编译器的名称。以下命令列出了ucc\ucl目录下的所有文件。

iron@ubuntu:ucl$ ls

alloc.c    error.c   input.h       reg.c      token.h       vector.h

alloc.h    error.h   keyword.h     reg.h      tokenop.h     x86.c

assert.c   expr.c    lex.c         simp.c     tranexpr.c    x86linux.c

ast.c      exprchk.c lex.h         stmt.c     tranexpr.uil  x86linux.tpl

ast.h      expr.h    linux         stmtchk.c  transtmt.c   x86win32.c

config.h   flow.c    Makefile      stmt.h     type.c        x86win32.tpl

decl.c     fold.c    Makefile.win  str.c      type.h

declchk.c  gen.c     opcode.h      str.h      ucl.c

decl.h     gen.h     opinfo.h      symbol.c   ucl.h

dumpast.c  grammer.h output.c      symbol.h   uildasm.c

emit.c     input.c   output.h      target.h   vector.c

iron@ubuntu:ucl$ ls | wc -w

61

    如上所示,目录ucc\ucl下共包含了61个文件,我们将按照词法分析、语法分析、语义检查、”中间代码生成及优化”和“目标代码生成”这样的脉络来剖析UCC编译器的源代码。当面对这么多的文件时,习惯上,我们会先看看Makefile文件。Linux平台上对应的Makefile文件如图2.1.1所示。

makefile文件怎么配置opencv_编译器

图2.1.1   Makefile

    在上述的Makefile中,我们看到了三个目标all、clean和test。”make all”是缺省的目标,图2.1.1的第12行用GCC编译器来编译UCC的源代码,并生成UCC编译器ucl;”make clean”则是通过第15行的rm命令删除”make all”过程中产生的后缀为”*.o”的目标文件和可执行程序ucl;而”make test”则用于做测试,用于测试的代码就是UCC编译器的源代码本身。图2.1.2表述了”make all”和”make test”的主要功能。

makefile文件怎么配置opencv_编译器_02

图2.1.2 make all和make test

    由图2.1.2,我们看到一个有趣的现象,UCC源代码经GCC编译后产生的可执行程序ucl本身就是一个C编译器。我们可以用ucl再来编译UCC源代码,得到一个新的编译器ucl1,接着还可以用ucl1来编译UCC源代码,生成编译器ucl2。这相当于UCC实现了“自我编译”,其源代码还起到了“测试代码”的作用。当然,这种测试是不完全的,能够自编译,不代表UCC源代码就不存在Bug了。例如,UCC编译器虽然可以为浮点运算产生相应的汇编指令,但在其自身C源代码中,并不存在浮点数运算。因此,UCC自编译的测试过程,并没有对浮点运算进行测试。相对而言,UCC原作者wenjunw@yahoo.cn发布的”ucc162.zip”,在浮点运算方面出现的Bug就较多。经sheisc@163.com修改后的版本” ucc162.2.tar.gz”虽然修改了一些Bug,但是可以百分之百确定的是,其中一定还包含不少Bug,甚至还可能存在修改代码时,引入新Bug的情况。限于人力和物力,UCC编译器在测试方面所做的工作还不太够。

接下来,让我们看一下UCC编译器的main()函数,如图2.1.3所示,第193行用于处理命令行参数,第195用于初始化管理寄存器的相关数据结构,第196行则是词法分析器的初始化,而第197行进行类型系统的初始化,各初始化设置的函数我们会在后面进行展开讨论。这里,我们主要通过main()函数,熟悉一下UCC编译器的内部工作流程。

makefile文件怎么配置opencv_语法分析_03

图2.1.3  UCL main()

    图2.1.3的第210至214行的for循环,用于依次编译命令行在出现的各个C文件。实际上严格说来,UCC驱动传递给UCC编译器的文件是经预处理后的文件,而不是原始的C文件。这里,我们在表述时,为简单起见,就把C文件当作C编译器的输入。实际上,我们在使用UCC编译器时,一般都是通过编译器驱动ucc命令来进行编译,很少会直接使用ucl命令。与编译相关的主要工作是在第212行的Compile()函数中实现。图2.1.4给出了Compile()函数的代码。

makefile文件怎么配置opencv_编译器_04

图2.1.4

Kernighan和C编译器的创始人Dennis M.Ritchie合著,江湖人尊称为《K&R》。建议在阅读UCC的语法分析器前,把这不到10页的C语言文法用打印机打印出来,对照着文法来阅读UCC语法分析器的源代码,同时,我们还会发现,UCC的原作者确实是用心良苦,分析函数的命名几乎都是与C文法中的非终结符一一对应,就如图2.1.4的第58行,我们一看函数名ParseTranslationUnit(),就能知道这是用于对编译单元进行语法分析的函数。第60检查一下在语法分析过程中是否出现语法错误,如果没有错误,则进入下一个阶段“语义检查”,如第64行的CheckTranslationUnit(transUnit)所示。通过语法分析,我们会构建一棵巨大的语法树,变量transUnit就指向这棵语法树的树根。而后续的语义检查就在这棵语法树上进行,所以第64行调用语义检查函数CheckTranslationUnit()时,传入了参数transUnit。

    语义检查时,需要根据IT大佬们制定的标准进行,文件ucc\ansi.c.txt是从http://flash-gordon.me.uk/ansi.c.txt下载来的C标准文档。为阅读方便,删去ucc\ansi.c.txt前面的一些客套话,就成了文件ucc\ansi_C_reference.txt。C语言历经几十年而始终屹立江湖,其标准本身也历经几次修订,比较知名的有C89、C99和C11。UCC的原作者是按C89进行编译器的构造。第64行实际上就是根据C89标准进行语义检查。前段时间,CSDN上有一篇对C++编译器早期实现者Stanley B.Lippman的一篇专访,Lippman很惬意地回顾了还没有C++标准委员会时,写C++编译器是何等之畅快,那时基本上就是他和C++创始人Bjarne Stroustrup商量一下,就可以上机实现。就如创业公司在只有几杆枪时,效率是何其之高,但规模变大后,就开始有政治,有江湖了。但是,产品要大规模推广,那就一定要标准化,这需要IT大佬们坐下来一起商量,在ucc\ansi.c.txt的前言部分我们能看到各大软件、硬件IT公司的大名。

    如果语义检查通过,如果使用UCC编译器的C程序员在命令行中加了参数” --dump-ast”,则UCC编译器就会把经语法分析和语义检查后形成的语法树打印到文件中,图2.1.4的72行完成了这个功能。接下来,就进入了中间代码生成阶段,如第76行的Translate(transUnit)所示,函数调用时的参数transUnit再次提醒我们,我们是在语法树的基础上来进行中间代码生成的。如果C程序员在命令行中指定了参数” --dump-IR”,则第81行会把中间代码打印到文件中。第87行的函数EmitTranslationUnit()则是用于产生x86汇编代码。

    以上就是UCC编译器的主要工作流程,每一阶段的具体过程我们会在后续章节依次进行分析。通过图2.1.3和图2.1.4,我们应可以在脑海中建立一个总体的轮廓。