本章先讲述本书以及编译器的概要,之后说明本书的示例程序C的安装方法。 1.1 本书的概要 本书的主题 本书的主题是编译器。编译器(compiler)是将编程语言的代码转换为其他形式的软件。这种转换操作就称为编译(compile)。 实际的编译器有C 语言的编译器GCC(GNU Compiler Collection)、Java 语言的编译器javac(Sun 公司)等。 像编译器这样复杂的软件,仅仅笼统地介绍一下是很难让人理解的,所以本书将从头开始制作一门语言的编译器。通过实际地设计、制作编译器,使读者对编译器产生具体、深刻的认识。这样通过实践获得的知识,在其他语言的编译器上也是通用的。 本书制作的编译器 本书将从头开始制作C♭A 这门语言的编译器。 C♭ 是笔者为本书设计的语言,基本上可以说是C 语言的子集。它在C 语言的基础上进行了简化,并加入了一些时兴的功能,使得与之配套的编译器制作起来比较容易。笔者最初想直接使用C 语言的,但是C 语言的编译器无论写起来还是读起来都非常难,所以最终放弃了。关于C♭ 的标准,第2 章会详细说明。 使用本书的C♭ 编译器编译出的程序是在PC 的Linux 平台上运行的。最近,借助虚拟机以及KNOPPIX 等,Linux 环境已经很容易搭建了。请读者一定要实际用C♭ 编译器编译程序,并尝试运行一下。 编译示例 接着让我们赶紧进入编译器的正题。首先我们来思考一下编译究竟是一种什么样的处理。这里以使用GCC 处理代码清单1.1 中的C 语言程 序为例进行说明。实际编译下面的程序时,需要重新安装GCC。 A ♭ 为降调符号(读音同“降”),表示把基本音符音高降低半音。——译者注 代码清单1.1 hello.c #include <stdio.h> int main(int argc, char **argv) { printf("Hello, World!n"); /* 打个招呼 */ return 0; } 本书的读者对象是已经掌握C 语言知识的人,所以理应编译过C 语言程序。但保险起见,还是确认一下编译的步骤。使用GCC 处理上述程序,需要输入如下命令。 $ gcc hello.c -o hello 这样便生成了名为hello 的文件,这是个可执行文件(executable file)。接着输入下面的命令,运行刚才生成的hello 命令。 $ ./hello Hello, World! 通过这样操作来运行程序本身没有问题,但从过程来看,还是有一些不明确的地方。 ●●可执行文件是怎样的文件 ●●gcc命令是如何生成可执行文件的 ●●可执行文件hello是经过哪些步骤运行起来的 让我们依次看一下上述疑问。 可执行文件 首先从GCC 生成的可执行文件是什么说起。 说到现代的Linux 上的可执行文件,通常是指符合ELF(Executable and Linking Format)这种特定形式的文件。ls、cp 这些命令(command)对应的实体文件都是可执行文件,例如/bin/ls 和/bin/cp 等。 使用file 命令能够查看文件是否符合ELF 的形式。例如,要查看/bin/ls 文件是不是ELF,在shell 中输入如下命令即可。 $ file /bin/ls /bin/ls: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux2.4.1, dynamically linked (uses shared libs), for GNU/Linux 2.4.1, stripped 如果像这样显示ELF......executable,就表示该文件为ELF 的可执行文件。根据所使用的Linux 机器的不同,可能显示ELF 64-bit,也可能显示ELF 32-bit MSB,这些都是ELF 的可执行文件。 ELF 文件中包含了程序(代码)以及如何运行该程序的相关信息(元数据)。程序(代码)就是机器语言(machine language)的列表。机器语言是唯一一种CPU 能够直接执行的语言,不同种类的CPU 使用不同的机器语言。 例如,现在基本上所有的个人计算机使用的都是Intel 公司的486 这款CPU 的后续产品,486 有着自己专用的机器语言。Sun 公司的SPARC 系列CPU 使用的是其他机器语言。IBM 公司的PowerPC 系列CPU 使用的又是不一样的机器语言。486 的机器语言不能在SPARC 上运行,反过来SPARC 的机器语言也不能在486 上运行。这点在SPARC 和PowerPC、486 和PowerPC上也一样。 GCC 将C 语言的程序转化为用机器语言(例如486 的机器语言)描述的程序。将机器语言的程序按照ELF 这种特定的文件格式注入文件,得到的就是可执行文件。 编译 那么gcc 命令是如何将hello.c 转换为可执行文件的呢?由hello.c 这样的单个文件来生成可执行文件时,虽然只需要执行一次gcc 命令,但实际上其内部经历了如下4 个阶段的处理。 1. 预处理 2.(狭义的)编译 3. 汇编 4. 链接 上述处理也可以统称为编译,但严谨地说第2 阶段进行的狭义的编译才是真正意义上的编译。本书中之后所提到的编译,指的就是狭义的编译。这4 个阶段的处理我们统称为build。下面对这4 个阶段的处理的作用进行简单的说明。 预处理C 语言的代码首先由预处理器(preprocessor)对#include 和#define 进行处理。具体来说,读入头文件,将所有的宏展开,这就是预处理(preprocess)。预处理的英文是 preprocess,就是前处理的意思。这里的“前”是在什么之前呢?当然是编译之前了。 预处理的内容近似于sed 命令和awk 命令这样的纯文本操作,不考虑C 语言语法的含义。 狭义的编译 接着,编译器对预处理器的输出进行编译,生成汇编语言(assemble language)的代码。一般来说,汇编语言的代码的文件扩展名是“.s”。汇编语言是由机器语言转换过来的人类较易阅读的文本形式的语言。机器语言是以CPU 的执行效率为第一要素设计的,用二进制代码表示,每一个bit 都有自己的含义,人类很难理解。因此,一般要使用与机器语言直接对应的汇编语言,以方便人们理解。 汇编 然后,汇编语言的代码由汇编器(assembler)转换为机器语言,这个处理过程称为汇编(assemble)。 汇编器的输出称为目标文件(object file)。一般来说,目标文件的扩展名是“.o”。Linux 中,目标文件也是ELF 文件。既然都是ELF 文件,那么究竟是目标文件还是可执行文件呢?这不是区分不了了吗?这个不用担心。ELF 文件中有用于提示文件种类的标志。例如,用file 命令来查看目标文件,会像下面这样显示ELF...relocatable,据此就能够将其和可执行文件区分开。 $ file t.o t.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped 链接 目标文件本身还不能直接使用,无论是直接运行还是作为程序库(library)文件调用都不可以。将目标文件转换为最终可以使用的形式的处理称为链接(link)。使用程序库的情况下,会在这个阶段处理程序库的加载 例如,假设Hello,World! 程序经过编译和汇编生成了目标文件hello.o,链接hello.o即可生成可执行文件。生成的可执行文件的默认文件名为a.out,可以使用gcc 命令的-o 选项来修改输出的文件名。 顺便提一下,通过链接处理生成的并不一定是可执行文件,也可以是程序库文件。程序库文件相关的话题将在第19 章中详细说明。 build 过程总结 如上所述,C 语言的代码经过预处理、编译、汇编、链接这4 个阶段的处理,最终生成可执行文件。图1.1 中总结了各个阶段的输出文件,我们再来确认一下。本书将对这4 个处理阶段中除预处理之外的编译、汇编和链接进行说明。 图1.1 生成可执行文件的过程 程序运行环境build① 的过程以链接为终点,但本书并不仅仅局限于build 的过程,还会涉及build 之后的程序运行环境相关的话题。从代码的编写、编译、运行到运行结束,理解上述全部过程是我们的目标。换言之,从编写完程序到该程序被运行,所有环节本书都会涉及(图1.2)。 图1.2 程序运行的全过程 为何除了build 的过程之外,本书还要涉及程序运行的环节呢?这是因为在现代编程语言的运行过程中,运行环境所起的作用越来越大。 首先,链接的话题并非仅仅出现在build 的过程中。如果使用了共享库,那么在开始运行程序时,链接才会发生。最近广泛使用的动态加载(dynamic load),就是一种将所有链接处理放到程序运行时进行的手法。 其次,像Java 和C# 这种语言的运行环境中都有垃圾回收(Garbage Collection,GC)这一强大的功能,该功能对程序的运行有着很大的影响。 再次,在Sun 的Java VM 等具有代表性的Java 的运行环境中,为了提高运行速度,采用了JIT 编译器(Just In Time compiler)。JIT 编译器是在程序运行时进行处理,将程序转换为机器 语言的编译器。也就是说,Java 语言是在运行时进行编译的。 ① build 有“构建”“生成”等译法,但似乎都不能表达出其全意,因此本书保留了英文用法 。——译者注 既然涉及了这样的话题,仅了解build 的过程是不够的,还必须了解程序的运行环境。不掌握包含运行环境在内的整个流程,就不能说完全理解了程序的动作。今后,无论是理解程序还是制作编译器,都需要了解从build 到运行环境的整体流程。 编程语言的运行方式 编译器会对程序进行编译,将其转换为可执行的形式。另外也有不进行编译,直接运行编程语言的方法。解释器(interpreter)就是这样一个例子。解释器不将程序转换为别的语言,而是直接 运行。例如Ruby 和Perl 的语言处理器就是用解释器来实现的。运行语言的手段不只一种。例如,C 语言也可以用解释器来解释执行,Ruby 也可以编译成机器语言或者Java 的二进制码。也就是说,编程语言与其运行方式可以自由搭配。因此,编译器也好,解释器也罢,都是处理并运行编程语言的手段之一,统称为编程语言处理器(programminglanguage processor )。 但是,根据语言的特点,其运行方式有适合、不适合该语言之说。一般来说,有静态类型检查(static type checking)、要求较高可靠性的情况下使用编译的方式;相反,没有静态类型检查、 对灵活性的要求高于严密性的情况下,则使用解释的方式。 静态类型检查是指在程序开始运行之前,对函数的返回值以及参数的类型进行检查的功能。与之相对,在程序运行过程中随时进行类型检查的方式称为动态类型检查(dynamic type checking) 。 这里提到的“动态”“静态”在语言处理器的话题中经常出现,所以最好记住。说到“静态”,就是指不运行程序而进行某些处理;说到“动态”,就是指一边运行程序一边进行某些处理。