C编译器剖析_1.4 UCC编译器预览_UCC驱动

    在上一小节,通过以下四条命令,我们有意地给ucc传递不同的参数来依次调用预处理器、编译器、汇编器和连接器。当然实际使用ucc命令时,我们不会绕这么大一个圈子,直接用”ucc  hello .c -o hello”即可得到可执行程序hello。

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

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

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

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

    这一小节,我们进入目录ucc\driver来分析一下ucc驱动器的源代码。图1.17给出了ucc\driver\linux.c中的部分代码。第17至22行是我们使用” ucc -E hello.c -o hello.i”命令时,ucc驱动真正执行的命令,其中的CPP代表的是C  PreProcessor,即C预处理器,而非C Plus Plus, C++。通过-D参数,指定了__GNUC__等预定义的宏,而$1、$2和$3用于充当占位符的作用,实际使用时会被程序员指定的的预处理参数所替代。例如当程序员输入以下命令时:

ucc -E -v hello.c -I../ -DREAL=double -o hello.ii

    命令中的” -I../ -DREAL=double”会被UCC Driver提取出来,用于替换字符串”$1”;而命令中的”hello.c”用于替换第21行的”$2”,充当预处理器的输入文件;命令中的”hello.ii”则替换”$3”,用来存放预处理后的结果。而”-v”参数会使UCC驱动在控制台上打印出实际所用到的命令,如下所示:

/usr/bin/gcc -U__GNUC__ -D_POSIX_SOURCE -D__STRICT_ANSI__ -Dunix-Di386 -Dlinux -D__unix__ -D__i386__ -D__linux__ -D__signed__=signed -D_UCC-I/home/iron/bin/include -I../ -DREAL=double -E hello.c -o hello.ii

   图中第33至36行是C编译器C Compiler对应的命令,当我们输入以下命令时

ucc -S -v hello.c --dump-ast -o hello.asm

   实际调用的是如下命令,第35行中$1、$2和$3的含义与第21行类似。而ucl就是我们后续要剖析的UCC编译器,相关源代码在\ucc\ucl中。图中第38至46行分别是汇编器Assembler和连接器Linker。第45行的参数"-lc","-lm"用来告诉连接器我们需要C函数库libc.so和数学运算函数库libm.so。

/home/iron/bin/ucl -o hello.asm --dump-ast hello.i

C编译器剖析_1.4 UCC编译器预览_UCC驱动_第1张图片

图1.17     编译工具链

    接下来的问题就是如何在ucc驱动中开启一个新进程来执行上述命令行所对应的预处理器、编译器、汇编器或者连接器。图1.18给出了在Linux平台上创建子进程及装载执行新程序的过程,其中关键的函数是由fork()、execv()和wait()这三个系统调用。Linux源于Unix,所以其系统调用fork()也源于Unix。生物界中,有许多细胞进行繁殖时,是通过细胞一分为二的分裂来进行,Linux创建子进程的系统调用fork()也相当于这个过程。英文fork是叉子的意思,叉子一分为二的形状也很形象地描绘了这个细胞分裂的过程。这实际上是一种克隆行为,clone。除了进程号PID、父进程号PPID及fork()返回值等少量信息不一样外,新创建的子进程和父进程几乎一模一样。这意味着当fork()成功后,会有两个进程并发执行以下第56行开始的代码,当然对这两个进程而言,因为fork()的返回值不一样,所以新创建的子进程会执行第63至66行的代码,而父进程则执行第72行之后的代码。第58至59行则是在创建新进程失败时执行。如果我们需要在子进程中加载外存中的某个可执行程序,则需要借助execv()这个系统调用。我们知道,编译链接后生成的可执行程序一般是存放在外存的,只有被装载器Loader从外存载入内存后,这个可执行程序才能运行。而Linux的execv()系统调用就是起到这个装载器的作用。如果子进程通过execv()加载了一个可执行程序,那对子进程而言,其整个进程空间中的代码段和数据段就会被替换成新加载的可执行程序的代码段和数据段。这意味着一旦第63行的execv()成功执行,子进程就再不会执行第64行之后的代码了。当然execv()不一定成功,原因可能是程序员在第63行的参数cmd中提供的路径不正确,或者该路径对应的文件根本就不是一个可执行程序等,这时会执行第64至66行的代码进行错误处理。父进程通过执行第72至73行中的系统调用wait()来等待子进程结束,这相当于父子进程之间的简单同步。只有在子进程执行完或者出错时,父进程才会执行第74至82行之间的代码。UCC驱动会通过函数ParseCmdLine()分析程序员提供的命令行参数,并结合图1.17中的CPPProg、CCProg、ASProg和LDProg的命令模板,通过函数BuildCommand()来创建实际要调用的命令,并将其作为参数传给图1.18中的Execute()函数。函数ParseCmdLine()和BuildCommand()的代码在在ucc\driver\ucc.c中,主要是进行一些字符串的处理,这里不再啰嗦。

C编译器剖析_1.4 UCC编译器预览_UCC驱动_第2张图片

图1.18 Execute()

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