第23章:编译程序
本章中,我们会学习如何通过编译源码来构建程序。源代码的可用性是使 Linux 成为可能的基本自由。整个 Linux 开发的生态系统依赖于开发者之间自由的交换。对于大多数桌面用户,编译是一门失落的艺术。编译在之前则更为常见,但是今天,发行版提供了维护庞大的预编译二进制的仓库,以供下载并使用。在本书写作之时, Debian 仓库(最大的发行版之一)包含了多于 68,000 个软件包。
那么,为何要编译软件呢?有两个理由:
可用性。尽管发行版仓库中有大量的预编译程序,有些发行版可能不会包含所有你想要的应用程序。在此情形下,要得到想要的程序,其唯一途径是从源码编译。
及时性。一些发行版专门研究最先进的程序版本,但也有很多不是这样。中意味着要使用最新版的程序,就必须编译。
从源码编译软件对许多用户来说过于复杂,需要太多的技术。然而,许多编译任务还是相当简单且仅涉及到很少的步骤。这取决于软件包。我们将学习一个简单的案例以提供该进程的一个概览,并且,作为那些想要继续深入学习的人的起点。
我们将介绍一个新命令:
make
维护程序的工具
什么是编译?
简单说来,编译是一个将源码(source code)(由程序员所写的一种人类可读的对于程序的描述)翻译成计算机处理器的本地语言的进程。
计算机的处理器(或 CPU)工作在一个基本水平,以机器语言(machine language)执行程序。这是一种数字编码,描述极其小的操作,诸如「加上这个字节」「指向内存中的这个位置」或「复制这个字节」。每一条这些指令都是由二进制表示的(0 和 1)。最早的计算机程序就是用这种数字码写成,这就可以解释为何写这些代码的程序员被说成抽很多烟、喝很多咖啡、并戴上厚厚的眼镜。
这些问题随着汇编语言(assembly language)的到来而被克服,它是用更易于使用的字符助记符(mnemonics),如 CPY(copy)和 MOV(move)。用汇编语言写成的程序由汇编器(assembler)处理为机器语言。直到今天,汇编语言还用在某些特定的编程任务,如设备驱动(device drivers)和嵌入式系统(embedded systems)。
接下来遇到的是高级编程语言(high-level programming languages)。被称为高级是因为它们允许程序员更少地去关注处理器正在做什么的细节,更多地去处理手上的事务。早期的高级语言(在 1950 年代开发的)包含 FORTRAN(为科学和技术任务而设计)和 COBOL(为商务应用而设计)。两者直到今天仍被有限地使用。
有很多流行的编程语言,占主导地位的有两个。多数为现代系统所写的程序是由 C 或 C++ 所写的。下面的示例中,我们将编译一个 C 程序。
由高级编程语言所写的程序,由另一种名为编译器(complier)的程序转换为机器语言。一些编译器将高级指令翻译到汇编语言,然后使用一个汇编器执行最后的翻译到机器语言的阶段。
有一个进程经常用来和编译连接,叫链接(linking)。会有很多由程序执行的公共任务。举个例子,如打开文件。很多程序都会执行这个任务,但是如果每个程序都实施各自的例程去打开文件,是很浪费的。由知道如何打开文件的单个程序去打开程序并允许所有需要它的程序共享,会更有意义。为通用任务提供支持的,由库(libraries)来完成。它们包含多个例程(routines),每个库都执行一些通用任务,可以被多个程序所共享。如果我们看 /lib
和 /usr/lib
目录,能看到许多这样的库。一个被称为链接器(linker)的程序用来在编译器的输出和已编译程序的需求之间建立连接。整个进程的最终结果就是一个可执行程序,供用户使用。
所有程序都是被编译的?
不是。我们已经看到,像 shell 脚本一样的程序,不需要编译。可以直接执行。这些是由那些脚本(scripting)或解释(interpreted)语言所编写的。这类语言在近年开始广泛流行起来,包括 Peal、Python、PHP、Ruby 还有很多。
脚本语言由一种称为解释器(interpreter)的特殊程序来执行。解释器输入程序文件,读取并执行文件中的每一条指令。通常地,解释型程序的执行速度要比编译型程序的慢很多。因为在解释型程序中的每一条源码指令在每次执行时都需要被翻译,而使用编译型程序,一条源码指令仅需被翻译一次,且翻译结果是被永久记录在最终得到的可执行文件中的。
为何解释型语言如此流行呢?因为对于许多零星的编程来说,这已经「足够快」了,但是真正的优势是相对于编译型程序,解释型程序通常更快且更容易开发。程序的开发过程通常是编码、编译、测试的循环。随着程序逐渐增长,编译阶段的周期可以变得相当长。解释型语言移除了编译步骤,所以加快了程序开发。
编译一个 C 程序
让我们来编译一些程序。在编译之前,需要某些像编译器一样的工具、链接器和 make
。在 Linux 环境中的 C 编译器通常是 gcc
(GNU C Compiler)最初由 Richard Stallman 编写。大多数发行版不会默认安装 gcc
。我们需要检查编译器是否可用:
上例中的结果指示编译器已经安装了。
提示:你的发行版可能有一个软件开发的元软件包(一个集成包)。有的话,如果你想要在系统中编译程序,可以考虑安装。如果你的系统没有提供元软件包,尝试安装
gcc
和make
包。在许多发行版中,这就足够完成下面的练习了。
获取源代码
对于编译练习,我们将编译来自 GNU 项目中的 diction
程序。这个小巧的程序用于检查文本文件的写作质量和风格。当程序运行时,相当小且易于构建。
遵循惯例,首先我们会创建一个名为 src
的目录来存放源代码,然后用 ftp
下载源码到该目录中。
除了上面例子中使用的传统的 ftp
外,还有很多途径可以下载源代码。例如 GNU 项目还支持用 HTTPS 来下载。我们可以用 wget
程序来下载 diction
的源码。
注意:由于当我们编译时是源码的「维护者」,我们需要将源码保存在
~/src
目录中。源码将由发行版安装在/usr/src
中,而我们维护的供多个用户使用的源码则通常被安装在/usr/local/src
。
我们可以看到,源码通常是以压缩的 tar
文件的形式提供的。有时候被称为一个压缩包(tarball),该文件包含了源码树(source tree)或者是目录阶层和包含源码的文件。在到达 ftp 站点之后,我们检查了可用的 tar 文件列表并选择下载最新版的文件。使用 ftp
中的 get
命令,我们将服务器中的文件复制到了本地机器中。
当下载 tar 文件后,必须将其解压。需要用到 tar
程序。
提示:
diction
程序与所有 GNU 项目的软件一样,遵循源码包装的某个标准。Linux 生态系统中大多数其它可用的源码也遵循该标准。标准的一个元素是,当源码压缩包被解压时,会创建一个包含源码树的目录,该目录会被命名为 project-x-xx,包含项目的名称和版本号。这种方案可以很方便地安装同一程序的多个版本。不过在解压前检查一下树的布局,是个好习惯。一些项目不会创建目录,只会直接将文件解压到当前目录。这会在我们原本井井有条的src
目录中制造混乱。要避免此类情形的发生,可以使用下面的命令检查 tar 文件的内容:
检查源码树
解压 tar 文件会创建一个新的目录,名为 diction-1.11
。该目录包含源码树。让我们观察其内部。
在目录内,我们看到很多文件。程序属于 GNU 项目,和其它许多程序一样,会提供文档文件 README
、INSTALL
、NEWS
和 COPYING
。这些文件包含了对程序的描述,如何建立并安装,以及其许可条款。在尝试建立程序前,仔细阅读 README
和 INSTALL
文件永远是个好主意。
该目录中有趣的文件还有以 .c
和 .h
结尾的那个文件。
.c
文件包含软件包内按模块区分的两个 C 程序(style
和 diction
)。将大型程序拆分为更小的、易于管理的文件,这是常见的实践。源代码文件是普通文本文件,可以由 less
查看。
.h
文件被识别为头文件(header files)。这些也是普通文本 。头文件包含常规描述,包含在源码文件或库中。为了让编译器能连接模块,必须让其能接收所有模块的描述,以完成整个程序。在 diction.c
文件的开头处,我们看到这一行:
这指示编译器在读取 diction.c
中的源代码时读取文件 getopt.h
,以「了解」getopt.c
中的内容。getopt.c
文件提供了由 style
和 diction
程序共享的例程。
在对 getopt.h
的 include
声明之前,我们看到一些其它的 include
声明:
这些也指向头文件,但是它们所指向的头文件在当前源码树之外。它们由系统提供对每个程序的汇编支持。如果我们看 /usr/include
目录,可以看到它们。
当我们安装编译器时,该目录中的头文件会被安装。
创建程序
创建大多数程序,使用两条命令序列。
configure
程序是一个由源码树内提供的 shell 脚本。它的工作是分析构建环境(the build environment)。多数源码会设计为可移植(portable)的。就是说,它被设计为可构建于多于一种的类 Unix 系统中。不过要做到这一点,源码可能需要在构建期间承担一些细微的调整,以适应系统间的差异。configure
还需要检查必要的外部工具和组件是否已安装。让我们运行 configure
。因为 configure
不是位于正常 shell 程序的所在位置,我们必须在命令前加 ./
明确地告知 shell,该程序位于当前工作目录。
configure
会在测试和配置构建时输出许多信息。当其完成后,看起来像下面所显示的那样:
这里重要的是,没有什么错误信息。如果有,会配置失败,程序只有在改正所有错误后才会被构建。
可以看到 configure
在源文件目录中创建了几个新文件。最重要的一个是 makefile。makefile 是个配置文件,指导 make
程序如何构建该程序。没有它,make
就拒绝运行。makefile 文件是一个普通的文本文件,所以可以这样查看:
make
程序将一个 makefile(通常名为 Makefile
)作为输入,该文件描述了组成最终程序的组件之间的关系和依赖性。
makefile 的第一部分定义了随后章节中会用到的变量。例如我们看到下面这一行:
那定义了 C 编译器为 gcc
。在该文件随后的内容中,我们看到它使用的实例。
这里会执行一次替换,$(CC)
的值在运行时会被替换为 gcc
。
大部分的 makefile 由那些定义目标(target)——本例中是可执行文件 diction
——及其依赖文件的行所组成。其余各行描述了从其组件创建目标所需的命令。我们看到例子中的可执行文件 diction
(最终产品之一)依赖于 diction.o
、sentence.o
、misc.o
、getopt.o
、getopt1.o
的存在。随后,在 makefile 中,我们看到了对这些目标的定义。
然而,我们看不到任何为这些文件指派的命令。这是由一个在文件前部的普通目标处理,它描述了用来将任意 .c
文件编译为一个 .o
文件的命令。
看起来非常复杂。为什么不简单地列出所有步骤以编译各部分并完成这些步骤呢?过一会儿就会知道答案了。同时,让我们运行 make
构建程序。
make
程序将会运行,使用 Makefile
文件中的内容来指导其行为。它会产生一大堆的信息。
当它完成的时候,我们会看到所有的目标现在都呈现在我们的目录中了。
这些文件之中,有了 diction
和 style
,我们所要构建的程序。恭喜,一切顺利!我们刚从源码编译了第一个程序!
不过,仅仅是出于好奇,让我们再次运行 make
。
它仅仅产生了这条奇怪的信息。发生了什么?为何不能再次构建程序?啊,这就是 make
的魔法了。make
只是构建需要构建的,而不是简单地再次构建。由于所有目标都已呈现,make
决定什么也不做了。我们可以删掉一个目标文件,重新运行 make
,来看它做了什么。让我们删掉一个中间目标。
我们看到 make
重建了该文件,并重新关联了 diction
和 style
程序,因为它们依赖了缺失的模块。这个行为还指出了 make
的另一个重要特性:它使目标保持其最新状态。make
坚持目标要比它们的依赖更新。这非常合理,因为程序员会经常更新一点源码,然后用 make
构建一个新版本的最终产品。make
保证所有事物需要基于更新过的源码构建。如果我们使用 touch
程序去「更新」一个源码文件,我们可以看一下发生了什么:
运行 make
之后,我们看到它已经使目标比其依赖项更新了:
make
智能地仅构建所需要构建的事物的能力,是程序员的一大福利。对于我们这些小项目来说,节约的时间可能不明显,但是对于更大项目来说,就具有重大意义了。记住,Linux 内核(一个承受着持续修改和更新的程序)包含着几百万行的代码。
安装程序
打包完好的源码总会包含一个特殊的 make
目标,install
。该目标会安装最终产品到系统目录供使用。通常,该目录是 /usr/local/bin
,传统上供本地构建软件的位置。然而,该目录对普通用户通常不是可写的,所以我们必须成为超级用户,才能执行安装。
在执行安装之后,我们可以检查程序是否已经准备待用。
成功了!
总结
本章中,我们看到了三个简单的命令:
是如何用来构建众多的源码包的。我们还看到了 make
在程序维护中的重要作用。make
程序可以用来维护目标程序及其依赖项之间的关系,而不仅仅是编译源码。
扩展阅读
维基百科有两篇关于编译器和
make
程序的文章:
Last updated
Was this helpful?