第二十三章:编译程序

在本章中,我们将介绍如何通过编译源代码来构建程序。源代码的可用性是使Linux成为可能的基本自由(essential freedom)。Linux开发的整个生态系统依赖于开发人员之间的自由交流。对于许多桌面用户来说,编译是一门失传的艺术。它曾经很常见,但今天,发行版提供商维护着巨大的预编译二进制文件存储库,随时可以下载和使用。在撰写本文时,Debian存储库(任何发行版中最大的存储库之一)包含68000多个软件包。

为什么要编译软件?有两个原因:

  1. 可用性。

    尽管发行版存储库中有许多预编译的程序,但一些发行版可能不包括所有所需的应用程序。在这种情况下,获得所需程序的唯一方法是从源代码编译它。

  2. 及时性。

    虽然一些发行版专注于尖端版本(cutting-edge)的程序,但许多发行版并没有。这意味着要获得最新版本的程序,必须进行编译。

从源代码编译软件可能会变得非常复杂和技术性,远远超出了许多用户的能力范围。然而,许多编译任务很容易,只涉及几个步骤。这一切都取决于包。我们将通过一个简单的案例来概述这一过程,并作为那些想要进行进一步研究的人的起点。

我们将介绍一个新命令:

第二十三章:编译程序什么是编译所有程序都是已编译的吗?编译一个C程序获取源代码检查源树构建程序安装程序总结

什么是编译

简单地说,编译是将源代码(source code,程序员编写的程序的人类可读描述)翻译成计算机处理器的本机(native)语言的过程。

计算机的处理器(或称 CPU)在基本级别(elemental level)上工作,以所谓的机器语言(machine language)执行程序。这是一个描述极小操作的数字代码,例如“添加此字节”、“指向内存中的此位置”或“复制此字节”。这些指令中的每一条都以二进制(1和0)表示。最早的计算机程序是使用这种数字代码编写的,这可能解释了为什么编写它的程序员据说吸烟很多,喝几加仑咖啡,戴厚眼镜。

这个问题被汇编语言(assembly language)的出现所克服,汇编语言用(稍微)更容易使用的字符助记符(mnemonics)取代了数字代码,如CPY (copy,复制)和MOV (move,移动)。用汇编语言编写的程序被称为汇编程序(assembler)的程序处理成机器语言。汇编语言今天仍然用于某些专门的编程任务,如设备驱动程序(device drivers)和嵌入式系统(embedded systems)。

接下来,我们将探讨所谓的高级编程语言(high-level programming languages)。之所以这样称呼,是因为它们让程序员不那么关心处理器正在做什么的细节,而更多地关心解决手头的问题。早期的(20世纪50年代开发的)包括 FORTRAN (专为科学和技术任务设计)和 COBOL (专为商业应用程序设计)。至今,两者仍然在有限领域使用。

虽然有许多流行的编程语言,但有两种占主导地位。大多数为现代系统编写的程序都是用 CC++ 编写的。在下面的例子中,我们将编译一个C程序。

用高级编程语言编写的程序通过用另一个程序(称为编译器,compiler)处理而转换为机器语言。一些编译器将高级指令翻译成汇编语言,然后使用汇编程序执行翻译成机器语言的最后阶段。

经常与编译结合使用的过程称为链接(linking)。程序执行许多常见任务。例如,打开一个文件。许多程序可以执行此任务,但让每个程序实现自己的例程来打开文件是浪费的。拥有一个知道如何打开文件并允许所有需要它的程序共享的单一程序更有意义。为常见任务提供支持是通过所谓的库(libraries)来实现的。它们包含多个例程,每个例程执行多个程序可以共享的一些常见任务。如果我们查看 /lib/usr/lib 目录,我们可以看到其中许多目录的位置。一个称为链接器(linker)的程序用于在编译器的输出和编译程序所需的库之间形成连接。此过程的最终结果是可执行程序文件(executable program file),可供使用。

所有程序都是已编译的吗?

不是。正如我们所看到的,有些程序(如shell脚本)不需要编译。他们被直接运行。这些是用所谓的脚本语言(scripting)或解释(interpreted)语言编写的。近年来,这些语言越来越受欢迎,包括 PerlPythonPHPRuby 和许多其他语言。

脚本语言由一个称为解释器(interpreter)的特殊程序执行。解释器输入程序文件,读取并执行其中包含的每条指令。一般来说,解释程序的执行速度比编译程序慢得多。这是因为解释程序中的每个源代码指令在每次执行时都会被翻译,而对于编译程序,源代码指令只翻译一次,并且这种翻译会永久记录在最终的可执行文件中。

为什么解释语言如此受欢迎?对于许多编程任务,结果“足够快”,但真正的优势是,开发解释程序通常比编译程序更快、更容易。程序通常是在代码、编译、测试的重复循环中开发的。随着程序规模的增长,周期的编译阶段可能会变得相当长。解释型语言消除了编译步骤,从而加快了程序开发。

编译一个C程序

让我们编译一些东西。然而,在我们这样做之前,我们需要一些工具,如编译器、链接器和 make 。在Linux环境中几乎普遍使用的C编译器称为 gcc (GNU C编译器),最初由Richard Stallman编写。默认情况下,大多数发行版不安装 gcc 。我们可以检查编译器是否像这样存在:

此示例中的结果表明已安装编译器。

提示:您的发行版可能有一个用于软件开发的元包(meta-package,包的集合)。如果是这样,如果您打算在系统上编译程序,请考虑安装它。如果您的系统没有提供元包,请尝试安装 gccmake 包。在许多发行版上,这足以进行以下练习。

获取源代码

对于我们的编译练习,我们将从GNU项目编译一个名为 diction 的程序。这个方便的小程序检查文本文件的写作质量和风格。就程序而言,它相当小,易于构建。

按照惯例,我们首先要为源代码创建一个名为 src 的目录,然后使用 ftp 将源代码下载到其中。

虽然我们在前面的示例中使用了传统的 ftp ,但还有其他下载源代码的方法。例如,GNU项目还支持使用HTTPS下载。我们可以使用 curl 程序下载 diction 源代码。

注意:由于我们在编译源代码时是它的“维护者”,我们将把它保存在 ~/src 中。您的发行版安装的源代码将安装在 /usr/src 中,而我们维护的供多个用户使用的源代码通常安装在 /usr/local/src 中。

正如我们所看到的,源代码通常以压缩tar文件的形式提供。有时称为 tarball ,此文件包含源代码树(source tree),或构成源代码的目录和文件的层次结构。到达ftp站点后,我们检查可用的tar文件列表,并选择最新版本进行下载。使用 ftp 中的 get 命令,我们将文件从 ftp 服务器复制到本地计算机。

下载tar文件后,必须将其解压缩。这是通过 tar 程序完成的:

提示:像所有GNU工程软件一样,diction 程序遵循某些源代码打包标准。Linux生态系统中可用的大多数其他源代码也遵循这一标准。该标准的一个要素是,当解压缩源代码tar文件时,将创建一个包含源代码树的目录,该目录将命名为 project-x.xx ,从而包含项目的名称和版本号。该方案允许轻松安装同一程序的多个版本。然而,在解包之前检查树的布局通常是一个好主意。一些项目不会创建目录,而是将文件直接传递到当前目录。这将在我们组织良好的 src 目录中造成混乱。为了避免这种情况,请使用以下命令检查 tar文件的内容:

tar tzvf tarfile | head

检查源树

解压缩tar文件会创建一个名为 diction-1.11 的新目录。此目录包含源代码树。让我们看看里面。

在其中,我们看到了许多文件。属于GNU项目的程序以及许多其他程序将提供文档文件README、INSTALL、NEWS和COPYING。这些文件包含程序的描述、如何构建和安装程序的信息及其许可条款。在尝试构建程序之前,仔细阅读READMEINSTALL文件总是一个好主意。

此目录中其他有趣的文件是以 .c.h 结尾的文件。

.c 文件包含该包提供的两个C程序(stylediction),分为模块。通常的做法是将大型程序分解为更小、更易于管理的部分。源代码文件是普通文本,可以用 less 进行查看。

.h 文件被称为头文件(header files)。这些也是普通的文本。头文件包含源代码文件或库中包含的例程的描述。为了使编译器连接模块,它必须收到完成整个程序所需的所有模块的描述。在 diction.c 文件的开头附近,我们看到以下行:

这指示编译器在读取 diction.c 中的源代码时读取文件 getopt.h ,以“知道” getopt.c 中的内容。 getopt.c 文件提供了 stylediction 程序共享的例程。

getopt.hinclude 语句之前,我们看到了一些其他的 include 语句,例如:

这些也指头文件,但它们指的是位于当前源代码树之外的头文件。它们由系统提供,以支持每个程序的编译。如果我们查看 /usr/include ,我们可以看到它们:

此目录中的头文件是在安装编译器时安装的。

构建程序

大多数程序都是用简单的两个命令序列构建的:

configure 程序是随源代码树提供的shell脚本。它的工作是分析构建环境。大多数源代码都是可移植的。也就是说,它被设计为构建在不止一种类Unix系统上。但要做到这一点,源代码可能需要在构建过程中进行轻微调整,以适应系统之间的差异。 configure 还会检查是否安装了必要的外部工具和组件。让我们运行 configure 。由于 configure 不在shell通常期望程序所在的位置,因此我们必须通过在命令前加 ./ 来明确告诉shell其位置,以表明程序位于当前工作目录中。

configure 在测试和配置构建时将输出大量消息。当它完成时,它看起来像这样:

这里重要的是没有错误消息。如果有,则配置失败,在纠正错误之前,程序将无法构建。

我们看到 configure 在源目录中创建了几个新文件。最重要的是 makefilemakefile 是一个配置文件,它精确地指示 make 程序如何构建程序。没有它, make 将拒绝运行。 makefile 是一个普通的文本文件,因此我们可以查看它:

make 程序将 makefile (通常称为 Makefile )作为输入,该文件描述了构成完成程序的组件之间的关系和依赖关系。

makefile 的第一部分定义了在 makefile 的后面部分中替换的变量。例如,我们看到以下行:

这将C编译器定义为 gcc 。在 makefile 的后面,我们看到一个使用它的实例。

这里执行替换,值 $(CC) 在运行时被 gcc 替换。

makefile 的大部分由定义目标(target)的行组成,在这种情况下是可执行文件 diction 和它所依赖的文件。其余几行描述了从其组件创建目标所需的命令。在这个例子中,我们看到可执行文件 diction (最终产品之一)取决于 diction.osentence.omisc.ogetopt.ogetopt1.o 的存在。稍后,在 makefile 中,我们将这些文件的定义视为目标。

但是,我们没有看到为它们指定的任何命令。这由文件前面的通用目标处理,该目标描述了用于将任何 .c 文件编译为 .o 文件的命令。

这一切似乎都很复杂。为什么不简单地列出编译零件的所有步骤并完成呢?这个问题的答案很快就会清楚。与此同时,让我们运行 make 并构建我们的程序。

make 程序将运行,使用 Makefile 的内容来指导其操作。它会产生很多信息。

当它完成时,我们将看到所有目标现在都存在于我们的目录中。

在这些文件中,我们看到了 dictionstyle ,以及我们开始构建的程序。恭喜你!我们刚刚从源代码编译了我们的第一个程序!

但只是出于好奇,让我们再来一次。

它只会产生这种奇怪的信息。怎么回事?它为什么不重新构建程序?啊,这就是 make 的魔力。与其简单地重新构建所有东西,make 只构建需要构建的东西。在所有目标都存在的情况下, make 确定没有什么可做的。我们可以通过删除其中一个目标并再次运行 make 来证明这一点,看看它能做什么。让我们去掉一个中间目标。

我们看到 make 会重建它,并重新链接 dictionstyle 程序,因为它们依赖于缺失的模块。这种行为还指出了 make 的另一个重要特征:它使目标保持最新。 make 坚持要求目标比其依赖项更新。这很有道理,因为程序员通常会更新一些源代码,然后使用 make 构建成品的新版本。 make 确保基于更新代码构建的所有内容都已构建。如果我们使用 touch 程序“更新”其中一个源代码文件,我们可以看到这种情况发生:

make 运行后,我们看到它已将目标恢复为比依赖项更新:

make 能够智能地只构建需要构建的东西,这对程序员来说是一个很大的好处。虽然我们的小型项目可能不会明显节省时间,但对于大型项目来说,这一点非常重要。记住,Linux内核(一个不断修改和改进的程序)包含数百万行代码。

安装程序

打包良好的(well-packaged)源代码通常会包含一个名为 install 的特殊 make 目标。此目标将在系统目录中安装最终产品以供使用。通常,此目录是 /usr/local/bin ,这是本地构建软件的传统位置。但是,普通用户通常无法写入此目录,因此我们必须成为超级用户才能执行安装。

执行安装后,我们可以检查程序是否已准备就绪。

我们成功了!

总结

在本章中,我们看到了三个简单的命令:

./configure make make install

可用于构建许多源代码包。我们还看到了在程序维护中发挥的重要作用。make 程序可用于任何需要维护目标/依赖关系的任务,而不仅仅是编译源代码。