【龙书笔记】编译器简介及程序构建过程综述

备注:本文是近期重新阅读编译器经典教材<Compilers Principles, Techniques, & Tools>一书(又称DragonBook,龙书)的其中一篇读书笔记。

1. 什么是编译器从本质来看,平时提到的“编程语言”其实都是一些助记符,用于向其他人或机器描述我们想要完成的逻辑运算。这些易于人类理解的语言想要被计算机理解并正确执行,就必须被转换成机器码,而完成这一转换过程的软件系统就是编译器。简言之,编译器其实也是一个计算机程序,它可以读取用一种编程语言(我们称之为source language)编写的代码并将其转换为用另一种语言(称之为target language)实现的程序,转换过程需保证前后两种语言描述的逻辑运算是等价的。这一过程如下图所示。

如果转换后的目标程序是可执行的机器语言程序,则我们可以直接调用之来输出预期结果,如下图所示。

根据上述描述,我们可以把编译器看作一个语言处理器(language processor)。另一种常见的语言处理器是解释器(interpreter),与编译器先生成目标程序然后执行不同,解释器是直接对输入执行源码中指定的操作,如下图所示。

由编译器生成的目标程序的运行效率通常要比解释执行的程序快的多且通常占用更少的资源,不过解释执行的程序通常更易于调试,其开发效率要高的多。这也是在处理器速度越来越快和存储器成本越来越低的今天,解释执行语言越来越流行的一大原因。值得一提的是Java的语言处理器,它融合了编译和解释执行两种行为:Java源码先被编译为字节码(bytecodes),然后由JVM解释执行。这种折衷处理方式使得Java程序与纯解释执行的语言相比具有一定性能优势;与纯编译型的语言相比又具有开发效率高且跨平台的能力(跨平台是因为JVM屏蔽了底层系统的差异)。这种具有混合行为的编译器如下图所示。

2. 由源码构建可执行程序的过程从严谨的角度看,由源码生成可执行程序的过程中,除编译器程序外,还需其它几个辅助工具程序,整个构建过程可用下图来说明。

由上图可知,由源码生成可执行程序的过程可以分解为4个步骤,分别是预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)和链接(Linking)。本文以编译C/C++程序为例来进行说明。2.1 预处理(预编译)这个过程主要处理源码中以"#"开头的预编译命令(如"include <xxx.h>"或#define xxx yyy等),主要规则如下:1) 将所有#define定义的宏做展开以替换原"#define"语句2) 处理所有的条件预编译指令,如"#if"/"#ifdef"/"#elif"/"#else"/"#endif"3) 处理"#include"预编译指令,将被包含的文件插入该预编译指令的位置。该过程是递归的,也即若#include指定的文件中include了其它文件,则这些文件都会被插入。4) 删除所有注释"//"和"/* */"5) 添加行号和文件名标识,如#2 "hello.c" 2,以便编译时产生用于调试的行号信息或编译过程输出Fatal或Warning时显示行号。6) 保留所有"#pragma"编译器指令,因为编译器会用到它们2.2 编译这个过程就是对预处理完的文件做词法分析(Lexical Analysis)、语法分析(Snytax Analysis)、语义分析(SemanticAnalysis)及优化,其输出是相应的汇编代码文件。这个过程是整个程序构建的核心部分,也是最复杂的部分。2.3 汇编汇编器将编译器生成的汇编代码转换成机器可执行的指令。汇编过程相对于编译过程来说比较简单,因为每个汇编语句几乎都对应一条机器指令,所以它没有复杂的语法,也没有语义,亦无需做指令优化,汇编器根据汇编指令和机器指令的映射表做翻译即可。2.4 链接链接就是把汇编器生成的*.o文件“组装”成最终的可执行程序。从原理上说,链接器的工作是把一些指令对其它符号地址的引用加以修正,也即把各个目标文件间相互引用的部分处理好,使得各个模块间能正确衔接。从链接时机来看,,链接可分为静态编译(或称静态链接)和动态链接两大类。静态链接主要包括地址和空间分配(Address and Storage Allocation)、符合决议(Symbol Resolution)和重定位(Relocation)等步骤。

3. Linux平台用gcc/g++构建可执行程序的过程分析本文第2部分介绍了从源码构建应用程序的通用过程,这里以Linux平台下gcc/g++构建c/c++程序为例进行具体说明,以期加深对这些过程的理解。首先需要明确的是,我们常提到的gcc/g++并非单独的可执行程序,从GCC的全称GNU Compiler Collection不难猜到,它们其实是由多个程序共同构成的工具链,用来从源码构建可执行程序。下面分别说明。假设现在有名为hello.c的文件需要构建成可执行程序,源码如下:#include <stdio.h>int main(){printf("Hello World\n");return 0;}3.1 gcc的预编译过程可以用下面的命令让gcc只进行预编译处理:$ gcc -E hello.c -o hello.i上例中,-E表示"Stop after the preprocessing stage; do not run the compiler proper",预编译的输出文件名后缀是.i,这是gcc约定的后缀,表示源码已被预处理,hello.i具体内容本文不赘述,感兴趣的话可以查看下。除-E选项外,预编译阶段还有很多其它控制选项,可以查看的gcc官网文档来了解。3.2 gcc的编译过程下面的命令可以对预编译后的文件hello.i进行编译,编译结果是汇编文件:$ gcc -S hello.i -o hello.s上例中,-S表示"Stop after the stage of compilation proper; do not assemble",其输出文件名后缀为.s,也是gcc的约定。如果查看hello.s文件,会发现其确实是汇编指令文件。现在版本的GCC工具集中,预编译和编译由同一个工具程序cc1(注意这里是数字1,不是字母l)来完成,其典型路径为"/usr/lib/gcc/i486-linux-gnu/4.9/cc1",其中4.9是gcc的版本号。可以直接调用ccl来完成预编译和编译:$ /usr/lib/gcc/i486-linux-gnu/4.9/cc1 hello.c它等价于下面的gcc命令:$ gcc -S hello.c -o hello.s3.3 gcc的汇编过程下面的gcc命令可以将hello.s汇编成目标文件:$ gcc -c hello.s -o hello.o也可以直接从源码开始产生目标文件:$ gcc -c hello.c -o hello.o其中-c选项表示"Compile or assemble the source files, but do not link"。此外,还可以直接调用gcc工具链中的汇编器as来完成汇编过程:$ as hello.s -o hello.o3.4 gcc的链接过程gcc工具链中的ld是静态链接器,下面的命令以-static选项用ld实现完全的静态链接程序(默认情况下,由gcc编译生成的可执行程序是动态链接的,其真正的链接过程发生在程序启动时):$ ld -static crt1.o crti.o crtbeginT.o hello.o -start-group -lgcc -lgcc_eh -lc -end-group crtend.o crtn.o其中,-static表示ld将以静态链接方式生成可执行文件(再次注意,gcc默认的链接方式是动态链接),该选项会影响其后紧跟的由-l指定的库的搜索方式,即紧跟在-static后的由-l选项指定的库的静态库版本会被搜索并参与链接过程。至于链接命令中由文件名或-l指定的库,一部分是C语言运行库glic(由-lc指定的libc库)及用来辅助实现启动/销毁机制的库(如crt1.o/crti.o/crtn.o),另一部分是gcc相关的库(如由-l指定的libgcc.a/libgcc_eh.a及由库名指定的crtbeginT.o/crtend.o)。这些库文件的用途建议参考《程序员的自我修养—链接、装载与库》一书第11.2.3小节的解释,这里略过。上面给出了通过-static选项进行静态编译的示例,事实上,gcc编译可执行程序时,默认采用动态链接方式生成执行程序,ld-linux.so.x是gcc工具链中的动态链接器(其中的x是版本号),以动态链接方式生成的可执行程序启动时,才会开始真正的链接过程。感兴趣的话,可通过gcc的-v选项来确定gcc在构建可执行程序过程中到底调用了哪些工具,这样可以加深对本文内容的理解。朋友,旭日正在升起,每一份付出,

【龙书笔记】编译器简介及程序构建过程综述

相关文章:

你感兴趣的文章:

标签云: