在本章中,我们将介绍如何通过编译源代码来构建程序。源代码的可用性是使Linux成为可能的基本自由(essential freedom)。Linux开发的整个生态系统依赖于开发人员之间的自由交流。对于许多桌面用户来说,编译是一门失传的艺术。它曾经很常见,但今天,发行版提供商维护着巨大的预编译二进制文件存储库,随时可以下载和使用。在撰写本文时,Debian存储库(任何发行版中最大的存储库之一)包含68000多个软件包。
为什么要编译软件?有两个原因:
可用性。
尽管发行版存储库中有许多预编译的程序,但一些发行版可能不包括所有所需的应用程序。在这种情况下,获得所需程序的唯一方法是从源代码编译它。
及时性。
虽然一些发行版专注于尖端版本(cutting-edge)的程序,但许多发行版并没有。这意味着要获得最新版本的程序,必须进行编译。
从源代码编译软件可能会变得非常复杂和技术性,远远超出了许多用户的能力范围。然而,许多编译任务很容易,只涉及几个步骤。这一切都取决于包。我们将通过一个简单的案例来概述这一过程,并作为那些想要进行进一步研究的人的起点。
我们将介绍一个新命令:
make
—— 维护程序的实用程序简单地说,编译是将源代码(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 (专为商业应用程序设计)。至今,两者仍然在有限领域使用。
虽然有许多流行的编程语言,但有两种占主导地位。大多数为现代系统编写的程序都是用 C 或 C++ 编写的。在下面的例子中,我们将编译一个C程序。
用高级编程语言编写的程序通过用另一个程序(称为编译器,compiler)处理而转换为机器语言。一些编译器将高级指令翻译成汇编语言,然后使用汇编程序执行翻译成机器语言的最后阶段。
经常与编译结合使用的过程称为链接(linking)。程序执行许多常见任务。例如,打开一个文件。许多程序可以执行此任务,但让每个程序实现自己的例程来打开文件是浪费的。拥有一个知道如何打开文件并允许所有需要它的程序共享的单一程序更有意义。为常见任务提供支持是通过所谓的库(libraries)来实现的。它们包含多个例程,每个例程执行多个程序可以共享的一些常见任务。如果我们查看 /lib 和 /usr/lib 目录,我们可以看到其中许多目录的位置。一个称为链接器(linker)的程序用于在编译器的输出和编译程序所需的库之间形成连接。此过程的最终结果是可执行程序文件(executable program file),可供使用。
不是。正如我们所看到的,有些程序(如shell脚本)不需要编译。他们被直接运行。这些是用所谓的脚本语言(scripting)或解释(interpreted)语言编写的。近年来,这些语言越来越受欢迎,包括 Perl 、 Python 、 PHP 、 Ruby 和许多其他语言。
脚本语言由一个称为解释器(interpreter)的特殊程序执行。解释器输入程序文件,读取并执行其中包含的每条指令。一般来说,解释程序的执行速度比编译程序慢得多。这是因为解释程序中的每个源代码指令在每次执行时都会被翻译,而对于编译程序,源代码指令只翻译一次,并且这种翻译会永久记录在最终的可执行文件中。
为什么解释语言如此受欢迎?对于许多编程任务,结果“足够快”,但真正的优势是,开发解释程序通常比编译程序更快、更容易。程序通常是在代码、编译、测试的重复循环中开发的。随着程序规模的增长,周期的编译阶段可能会变得相当长。解释型语言消除了编译步骤,从而加快了程序开发。
让我们编译一些东西。然而,在我们这样做之前,我们需要一些工具,如编译器、链接器和 make
。在Linux环境中几乎普遍使用的C编译器称为 gcc
(GNU C编译器),最初由Richard Stallman编写。默认情况下,大多数发行版不安装 gcc
。我们可以检查编译器是否像这样存在:
xxxxxxxxxx
[me@linuxbox ~]$ which gcc
/usr/bin/gcc
此示例中的结果表明已安装编译器。
提示:您的发行版可能有一个用于软件开发的元包(meta-package,包的集合)。如果是这样,如果您打算在系统上编译程序,请考虑安装它。如果您的系统没有提供元包,请尝试安装 gcc
和 make
包。在许多发行版上,这足以进行以下练习。
对于我们的编译练习,我们将从GNU项目编译一个名为 diction
的程序。这个方便的小程序检查文本文件的写作质量和风格。就程序而言,它相当小,易于构建。
按照惯例,我们首先要为源代码创建一个名为 src 的目录,然后使用 ftp
将源代码下载到其中。
xxxxxxxxxx
[me@linuxbox ~]$ mkdir src
[me@linuxbox ~]$ cd src
[me@linuxbox src]$ ftp ftp.gnu.org
Connected to ftp.gnu.org.
220 GNU FTP server ready.
Name (ftp.gnu.org:me): anonymous
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> cd gnu/diction
250 Directory successfully changed.
ftp> ls
200 PORT command successful. Consider using PASV.
150 Here comes the directory listing.
-rw-r--r-- 1 1003 65534 68940 Aug 28 1998 diction-0.7.tar.gz
-rw-r--r-- 1 1003 65534 90957 Mar 04 2002 diction-1.02.tar.gz
-rw-r--r-- 1 1003 65534 141062 Sep 17 2007 diction-1.11.tar.gz
226 Directory send OK.
ftp> get diction-1.11.tar.gz
local: diction-1.11.tar.gz remote: diction-1.11.tar.gz
200 PORT command successful. Consider using PASV.
150 Opening BINARY mode data connection for diction-1.11.tar.gz (141062 bytes).
226 File send OK.
141062 bytes received in 0.16 secs (847.4 kB/s)
ftp> bye
221 Goodbye.
[me@linuxbox src]$ ls
diction-1.11.tar.gz
虽然我们在前面的示例中使用了传统的 ftp
,但还有其他下载源代码的方法。例如,GNU项目还支持使用HTTPS下载。我们可以使用 curl
程序下载 diction 源代码。
xxxxxxxxxx
[me@linuxbox ~]$ curl -O https://ftp.gnu.org/gnu/diction/diction-1.11.tar.gz
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 137k 100 137k 0 0 37118 0 0:00:03 0:00:03 --:--:-- 37121
注意:由于我们在编译源代码时是它的“维护者”,我们将把它保存在 ~/src 中。您的发行版安装的源代码将安装在 /usr/src 中,而我们维护的供多个用户使用的源代码通常安装在 /usr/local/src 中。
正如我们所看到的,源代码通常以压缩tar文件的形式提供。有时称为 tarball
,此文件包含源代码树(source tree),或构成源代码的目录和文件的层次结构。到达ftp站点后,我们检查可用的tar文件列表,并选择最新版本进行下载。使用 ftp
中的 get
命令,我们将文件从 ftp
服务器复制到本地计算机。
下载tar文件后,必须将其解压缩。这是通过 tar
程序完成的:
xxxxxxxxxx
[me@linuxbox src]$ tar xzf diction-1.11.tar.gz
[me@linuxbox src]$ ls
diction-1.11 diction-1.11.tar.gz
提示:像所有GNU工程软件一样,diction
程序遵循某些源代码打包标准。Linux生态系统中可用的大多数其他源代码也遵循这一标准。该标准的一个要素是,当解压缩源代码tar文件时,将创建一个包含源代码树的目录,该目录将命名为 project-x.xx ,从而包含项目的名称和版本号。该方案允许轻松安装同一程序的多个版本。然而,在解包之前检查树的布局通常是一个好主意。一些项目不会创建目录,而是将文件直接传递到当前目录。这将在我们组织良好的 src 目录中造成混乱。为了避免这种情况,请使用以下命令检查 tar文件的内容:
tar tzvf tarfile | head
解压缩tar文件会创建一个名为 diction-1.11 的新目录。此目录包含源代码树。让我们看看里面。
xxxxxxxxxx
[me@linuxbox src]$ cd diction-1.11
[me@linuxbox diction-1.11]$ ls
config.guess diction.c getopt.c nl
config.h.in diction.pot getopt.h nl.po
config.sub diction.spec getopt_int.h README
configure diction.spec.in INSTALL sentence.c
configure.in diction.texi.in install-sh sentence.h
COPYING en Makefile.in style.1.in
de en_GB misc.c style.c
de.po en_GB.po misc.h test
diction.1.in getopt1.c NEWS
在其中,我们看到了许多文件。属于GNU项目的程序以及许多其他程序将提供文档文件README、INSTALL、NEWS和COPYING。这些文件包含程序的描述、如何构建和安装程序的信息及其许可条款。在尝试构建程序之前,仔细阅读README和INSTALL文件总是一个好主意。
此目录中其他有趣的文件是以 .c 和 .h 结尾的文件。
xxxxxxxxxx
[me@linuxbox diction-1.11]$ ls *.c
diction.c getopt1.c getopt.c misc.c sentence.c style.c
[me@linuxbox diction-1.11]$ ls *.h
getopt.h getopt_int.h misc.h sentence.h
.c 文件包含该包提供的两个C程序(style 和 diction),分为模块。通常的做法是将大型程序分解为更小、更易于管理的部分。源代码文件是普通文本,可以用 less
进行查看。
xxxxxxxxxx
[me@linuxbox diction-1.11]$ less diction.c
.h 文件被称为头文件(header files)。这些也是普通的文本。头文件包含源代码文件或库中包含的例程的描述。为了使编译器连接模块,它必须收到完成整个程序所需的所有模块的描述。在 diction.c 文件的开头附近,我们看到以下行:
xxxxxxxxxx
这指示编译器在读取 diction.c 中的源代码时读取文件 getopt.h ,以“知道” getopt.c 中的内容。 getopt.c 文件提供了 style 和 diction 程序共享的例程。
在 getopt.h 的 include
语句之前,我们看到了一些其他的 include
语句,例如:
xxxxxxxxxx
这些也指头文件,但它们指的是位于当前源代码树之外的头文件。它们由系统提供,以支持每个程序的编译。如果我们查看 /usr/include ,我们可以看到它们:
xxxxxxxxxx
[me@linuxbox diction-1.11]$ ls /usr/include
此目录中的头文件是在安装编译器时安装的。
大多数程序都是用简单的两个命令序列构建的:
xxxxxxxxxx
./configure
make
configure
程序是随源代码树提供的shell脚本。它的工作是分析构建环境。大多数源代码都是可移植的。也就是说,它被设计为构建在不止一种类Unix系统上。但要做到这一点,源代码可能需要在构建过程中进行轻微调整,以适应系统之间的差异。 configure
还会检查是否安装了必要的外部工具和组件。让我们运行 configure
。由于 configure
不在shell通常期望程序所在的位置,因此我们必须通过在命令前加 ./
来明确告诉shell其位置,以表明程序位于当前工作目录中。
xxxxxxxxxx
[me@linuxbox diction-1.11]$ ./configure
configure
在测试和配置构建时将输出大量消息。当它完成时,它看起来像这样:
xxxxxxxxxx
checking libintl.h presence... yes
checking for libintl.h... yes
checking for library containing gettext... none required
configure: creating ./config.status
config.status: creating Makefile
config.status: creating diction.1
config.status: creating diction.texi
config.status: creating diction.spec
config.status: creating style.1
config.status: creating test/rundiction
config.status: creating config.h
[me@linuxbox diction-1.11]$
这里重要的是没有错误消息。如果有,则配置失败,在纠正错误之前,程序将无法构建。
我们看到 configure
在源目录中创建了几个新文件。最重要的是 makefile 。 makefile 是一个配置文件,它精确地指示 make
程序如何构建程序。没有它, make
将拒绝运行。 makefile 是一个普通的文本文件,因此我们可以查看它:
xxxxxxxxxx
[me@linuxbox diction-1.11]$ less Makefile
make
程序将 makefile (通常称为 Makefile )作为输入,该文件描述了构成完成程序的组件之间的关系和依赖关系。
makefile 的第一部分定义了在 makefile 的后面部分中替换的变量。例如,我们看到以下行:
xxxxxxxxxx
CC= gcc
这将C编译器定义为 gcc
。在 makefile 的后面,我们看到一个使用它的实例。
xxxxxxxxxx
diction: diction.o sentence.o misc.o getopt.o getopt1.o
$(CC) -o $@ $(LDFLAGS) diction.o sentence.o misc.o \
getopt.o getopt1.o $(LIBS)
这里执行替换,值 $(CC)
在运行时被 gcc
替换。
makefile 的大部分由定义目标(target)的行组成,在这种情况下是可执行文件 diction 和它所依赖的文件。其余几行描述了从其组件创建目标所需的命令。在这个例子中,我们看到可执行文件 diction (最终产品之一)取决于 diction.o 、 sentence.o 、misc.o 、 getopt.o 和 getopt1.o 的存在。稍后,在 makefile 中,我们将这些文件的定义视为目标。
xxxxxxxxxx
diction.o: diction.c config.h getopt.h misc.h sentence.h
getopt.o: getopt.c getopt.h getopt_int.h
getopt1.o: getopt1.c getopt.h getopt_int.h
misc.o: misc.c config.h misc.h
sentence.o: sentence.c config.h misc.h sentence.h
style.o: style.c config.h getopt.h misc.h sentence.h
但是,我们没有看到为它们指定的任何命令。这由文件前面的通用目标处理,该目标描述了用于将任何 .c 文件编译为 .o 文件的命令。
xxxxxxxxxx
.c.o:
$(CC) -c $(CPPFLAGS) $(CFLAGS) $<
这一切似乎都很复杂。为什么不简单地列出编译零件的所有步骤并完成呢?这个问题的答案很快就会清楚。与此同时,让我们运行 make
并构建我们的程序。
xxxxxxxxxx
[me@linuxbox diction-1.11]$ make
make
程序将运行,使用 Makefile 的内容来指导其操作。它会产生很多信息。
当它完成时,我们将看到所有目标现在都存在于我们的目录中。
xxxxxxxxxx
[me@linuxbox diction-1.11]$ ls
config.guess de.po en install-sh sentence.c
config.h diction en_GB Makefile sentence.h
config.h.in diction.1 en_GB.mo Makefile.in sentence.o
config.log diction.1.in en_GB.po misc.c style
config.status diction.c getopt1.c misc.h style.1
config.sub diction.o getopt1.o misc.o style.1.in
configure diction.pot getopt.c NEWS style.c
configure.in diction.spec getopt.h nl style.o
COPYING diction.spec.in getopt_int.h nl.mo test
de diction.texi getopt.o nl.po
de.mo diction.texi.in INSTALL README
在这些文件中,我们看到了 diction 和 style ,以及我们开始构建的程序。恭喜你!我们刚刚从源代码编译了我们的第一个程序!
但只是出于好奇,让我们再来一次。
xxxxxxxxxx
[me@linuxbox diction-1.11]$ make
make: Nothing to be done for `all'.
它只会产生这种奇怪的信息。怎么回事?它为什么不重新构建程序?啊,这就是 make
的魔力。与其简单地重新构建所有东西,make
只构建需要构建的东西。在所有目标都存在的情况下, make
确定没有什么可做的。我们可以通过删除其中一个目标并再次运行 make
来证明这一点,看看它能做什么。让我们去掉一个中间目标。
xxxxxxxxxx
[me@linuxbox diction-1.11]$ rm getopt.o
[me@linuxbox diction-1.11]$ make
我们看到 make
会重建它,并重新链接 diction 和 style 程序,因为它们依赖于缺失的模块。这种行为还指出了 make
的另一个重要特征:它使目标保持最新。 make
坚持要求目标比其依赖项更新。这很有道理,因为程序员通常会更新一些源代码,然后使用 make
构建成品的新版本。 make
确保基于更新代码构建的所有内容都已构建。如果我们使用 touch
程序“更新”其中一个源代码文件,我们可以看到这种情况发生:
xxxxxxxxxx
[me@linuxbox diction-1.11]$ ls -l diction getopt.c
-rwxr-xr-x 1 me me 37164 2009-03-05 06:14 diction
-rw-r--r-- 1 me me 33125 2007-03-30 17:45 getopt.c
[me@linuxbox diction-1.11]$ touch getopt.c
[me@linuxbox diction-1.11]$ ls -l diction getopt.c
-rwxr-xr-x 1 me me 37164 2009-03-05 06:14 diction
-rw-r--r-- 1 me me 33125 2009-03-05 06:23 getopt.c
[me@linuxbox diction-1.11]$ make
make
运行后,我们看到它已将目标恢复为比依赖项更新:
xxxxxxxxxx
[me@linuxbox diction-1.11]$ ls -l diction getopt.c
-rwxr-xr-x 1 me me 37164 2009-03-05 06:24 diction
-rw-r--r-- 1 me me 33125 2009-03-05 06:23 getopt.c
make
能够智能地只构建需要构建的东西,这对程序员来说是一个很大的好处。虽然我们的小型项目可能不会明显节省时间,但对于大型项目来说,这一点非常重要。记住,Linux内核(一个不断修改和改进的程序)包含数百万行代码。
打包良好的(well-packaged)源代码通常会包含一个名为 install 的特殊 make
目标。此目标将在系统目录中安装最终产品以供使用。通常,此目录是 /usr/local/bin ,这是本地构建软件的传统位置。但是,普通用户通常无法写入此目录,因此我们必须成为超级用户才能执行安装。
xxxxxxxxxx
[me@linuxbox diction-1.11]$ sudo make install
执行安装后,我们可以检查程序是否已准备就绪。
xxxxxxxxxx
[me@linuxbox diction-1.11]$ which diction
/usr/local/bin/diction
[me@linuxbox diction-1.11]$ man diction
我们成功了!
在本章中,我们看到了三个简单的命令:
./configure make make install
可用于构建许多源代码包。我们还看到了在程序维护中发挥的重要作用。make
程序可用于任何需要维护目标/依赖关系的任务,而不仅仅是编译源代码。