C编译器剖析_1.4 UCC编译器预览_UCC的使用

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

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 [email protected]” ,编译器看到的是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完成。在此阶段出现的错误有,全局变量(或函数)重复定义,全局变量(或函数)未定义等。

     hello.c  ---> hello.i  ---> hello.s  ---> hello.o  ---> hello

    我们可通过以下命令来验证上述过程。

    iron@ubuntu:demo$ ls

    build.bat  hello.c  Makefile

    iron@ubuntu:demo$ ucc -E hello.c -o hello.i

    iron@ubuntu:demo$ ls

    build.bat  hello.c  hello.i Makefile

    iron@ubuntu:demo$ ucc --dump-ast --dump-IR -S hello.i -o hello.s

    iron@ubuntu:demo$ ls

    build.bat  hello.ast  hello.c hello.i  hello.s  hello.uil Makefile

    iron@ubuntu:demo$ ucc -c hello.s -o hello.o

    iron@ubuntu:demo$ ls

    build.bat  hello.ast  hello.c hello.i  hello.o  hello.s hello.uil  Makefile

    iron@ubuntu:demo$ ucc hello.o -o hello

    iron@ubuntu:demo$ ls

    build.bat  hello.ast  hello.i hello.s    Makefile

    hello      hello.c    hello.o hello.uil

    iron@ubuntu:demo$ ./hello

f(1) = 1

f(2) = 2

f(3) = 6

f(4) = 24

f(5) = 120

f(6) = 720

f(7) = 5040

f(8) = 40320

f(9) = 362880

f(10) = 3628800

    按上面的分析,我们可以发现,要由C源代码得到一份可执行程序,我们需要“预处理器、编译器、汇编器和连接器”这么多工具的配合,这实际上是一套工具链,平时我们讲用GCC编译器,实际上我们是用这套工具链中的编译器来作为这套工具链的代表了。UCC的原作者完成的是C编译器,而预处理器、汇编器和连接器仍然用Linux 或Windows上的相应工具。当然在上述命令中,我们只用了ucc,但给ucc传递了–E等参数来通知ucc调用预处理器等工具。在UCC编译器阅读和分析时,比较有用的参数还有”--dump-ast --dump-IR”,用于生成字符串形式表达的抽象语法树AST,及优化后的中间代码IR。

    换言之,上述的ucc命令实际上是个编译器驱动driver,用来驱动编译工具链的预处理器、编译器、汇编器和连接器。UCC驱动的代码在ucc\driver目录中,而我们要剖析的UCC编译器的源代码则在\ucc\ucl。按“扫外围,剪裙边”的思路,下一小节,我们会先剖析一下UCC编译器驱动的代码。


本节附录,由hello.c生成的语法树、中间代码和汇编代码如下所示:

抽象语法树hello.ast

function f

{

  (if (< n

          1)

    (then 

      {

        (ret 1)

      }

    end-then)

    (else 

      {

        (ret (* n

                (call f

                      (- n

                         1))))

      }

    end-else)

  end-if)

}

 

function main

{

  (while (<= i

              10)

    {

      (call printf

            str0,

            i,

            (call f

                  i))

 

      (+= i

          (+ i

             1))++

    }

  end-while)

 

  (ret 0)

}

 

中间代码hello.uil

function f

         if (n >= 1) goto BB3;

 

BB1: ref= 0, sym->ref = 0 ,npred = 1 , nsucc = 1

         return 1;

         goto BB4;

         goto BB4;

 

BB3: ref= 1, sym->ref = 0 ,npred = 1 , nsucc = 1

         t0 : n + -1;

         t1 : f(t0);

         t2 : n * t1;

         return t2;

 

BB4: ref= 2, sym->ref = 0 ,npred = 3 , nsucc = 0

         ret

 

 

function main

         i = 1;

         goto BB7;

 

BB6: ref= 1, sym->ref = 0 ,npred = 1 , nsucc = 1

         t0 :&str0;

         t1 : f(i);

         printf(t0, i, t1);

         ++i;

 

BB7: ref= 1, sym->ref = 0 ,npred = 2 , nsucc = 2

         if (i <= 10) goto BB6;

 

BB8: ref= 0, sym->ref = 0 ,npred = 1 , nsucc = 0

         return 0;

         ret

 

 

x86汇编代码  hello.s

 

# Codeauto-generated by UCC

 

.data

 

.str0:        .string      "f(%d)= %d\012"

 

 

 

.text

 

.globl        f

 

f:

         pushl %ebp

         pushl %ebx

         pushl %esi

         pushl %edi

         movl %esp, %ebp

         subl $12, %esp

.BB0:

         cmpl $1, 20(%ebp)

         jge .BB3

.BB1:

         movl $1, %eax

         jmp .BB4

.BB2:

         jmp .BB4

.BB3:

         movl 20(%ebp), %eax

         addl $-1, %eax

         pushl %eax

         call f

         addl $4, %esp

         movl 20(%ebp), %ecx

         imull %eax, %ecx

         movl %ecx, %eax

.BB4:

         movl %ebp, %esp

         popl %edi

         popl %esi

         popl %ebx

         popl %ebp

         ret

 

.globl        main

 

main:

         pushl %ebp

         pushl %ebx

         pushl %esi

         pushl %edi

         movl %esp, %ebp

         subl $12, %esp

.BB5:

         movl $1, -4(%ebp)

         jmp .BB7

.BB6:

         leal .str0, %eax

         pushl -4(%ebp)

         movl %eax, -8(%ebp)

         call f

         addl $4, %esp

         pushl %eax

         pushl -4(%ebp)

         pushl -8(%ebp)

         call printf

         addl $12, %esp

         incl -4(%ebp)

.BB7:

         cmpl $10, -4(%ebp)

         jle .BB6

.BB8:

         movl $0, %eax

         movl %ebp, %esp

         popl %edi

         popl %esi

         popl %ebx

         popl %ebp

         ret

       

     其中”BB”是基本块Basic Block的意思。如前文1.3节在为if和while语句生成中间代码时所述,有条件或无条件的跳转会引起控制流的转移。当然,函数调用也可视为一种无条件跳转,因为函数调用所做的工作包括把传递实参、返回地址入栈及无条件跳转到函数起始地址。在进行代码优化时,我们需要考虑控制流的变化。我们期望控制流只能从基本块的第一条指令进入,离开时从基本块中的最后一条指令离开。而整个程序就由若干个基本块构成,而跳转指令就可看成连接两个基本块的边,于是在中间代码生成后,我们就把整个程序看成数据结构中的“图”,这个由基本块构成的图被称为控制流图control flow graph,简写为CFG。后续的分析和优化都是基于“控制流图”这样的数据结构进行。当然,需要说明的是,UCC并未把函数调用视为控制流的转移,例如上述基本块“.BB6:”,其中” call printf”是对printf函数的调用,但UCC并未将其当作基本块的最后一条指令。原因在于UCC的没有做太多的优化,并未作复杂的过程间分析,其优化几乎只在基本块内进行,在分析ucc\ucl\simp.c中的PeepHole()函数和ucc\ucl\x86.c的EmitBBlock()函数时我们会进一步展开。对照着hello.c的源代码,即便从未学过汇编语言,也能对hello.ast、hello.uil和hello.s中的代码猜出个大概意思。



你可能感兴趣的:(C编译器剖析)