2020-04-10
什么是编译?
简而言之,编译就是把源码(一个由程序员编写的人类可读的程序的说明)翻译成计算机处理器的语言的过程。
所有的程序都是可编译的吗?
不是。正如我们所看到的,有些程序比如 shell 脚本就不需要编译。它们直接执行。这些程序是用所谓的脚本或解释型语言编写的。近年来,这些语言变得越来越流行,包括 Perl、Python、PHP、Ruby 和许多其它语言。
脚本语言由一个叫做解释器的特殊程序执行。一个解释器输入程序文件,读取并执行程序中包含的每一条指令。通常来说,解释型程序执行起来要比编译程序慢很多。这是因为每次解释型程序执行时,程序中每一条源码指令都需要翻译,而一个已经编译好的程序,一条源码指令只翻译了一次,翻译后的指令会永久地记录到最终的执行文件中。
那么为什么解释型程序这样流行呢?对于许多编程任务来说,原因是“足够快”,但是真正的优势是一般来说开发解释型程序要比编译程序快速且容易。通常程序开发需要经历一个不断重复的写码、编译和测试周期。随着程序变得越来越大,编译阶段会变得相当耗时。解释型语言删除了编译步骤,这样就加快了程序开发。
编译一个 C 语言
在我们编译之前,然而我们需要一些工具,像编译器、链接器以及make
。在 Linux 环境中,普遍使用的 C 编译器叫做 gcc
(GNU C 编译器),最初由 Richard Stallman 写出来的。大多数 Linux 系统发行版默认不安装 gcc
。我们可以这样查看该编译器是否存在:
[root@VM_0_7_centos ~]# which gcc
/usr/bin/gcc
在这个例子中的输出结果表明安装了 gcc
编译器。
得到源码
为了我们的编译练习,我们将编译一个叫做 diction 的程序,来自 GNU 项目。这是一个小巧方便的程序,检查文本文件的书写质量和样式。就程序而言,它相当小,且容易创建。
遵照惯例,首先我们要创建一个名为 src
的目录来存放我们的源码,然后使用 ftp
协议把源码下载下来。
[root@VM_0_7_centos ~]# mkdir src
[root@VM_0_7_centos ~]# cd src/
[root@VM_0_7_centos src]# ftp ftp.gnu.org
Trying 209.51.188.20...
Connected to ftp.gnu.org (209.51.188.20).
220 GNU FTP server ready.
Name (ftp.gnu.org:root): anonymous
230-NOTICE (Updated October 13 2017):
230-
230-Because of security concerns with plaintext protocols, we still
230-intend to disable the FTP protocol for downloads on this server
230-(downloads would still be available over HTTP and HTTPS), but we
230-will not be doing it on November 1, 2017, as previously announced
230-here. We will be sharing our reasons and offering a chance to
230-comment on this issue soon; watch this space for details.
230-
230-If you maintain scripts used to access ftp.gnu.org over FTP,
230-we strongly encourage you to change them to use HTTPS instead.
230-
230----
230-
230-Due to U.S. Export Regulations, all cryptographic software on this
230-site is subject to the following legal notice:
230-
230- This site includes publicly available encryption source code
230- which, together with object code resulting from the compiling of
230- publicly available source code, may be exported from the United
230- States under License Exception "TSU" pursuant to 15 C.F.R. Section
230- 740.13(e).
230-
230-This legal notice applies to cryptographic software only. Please see
230-the Bureau of Industry and Security (www.bxa.doc.gov) for more
230-information about current U.S. regulations.
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> cd gnu/diction
250 Directory successfully changed.
ftp> sl
?Invalid command
ftp> ls
227 Entering Passive Mode (209,51,188,20,113,59).
150 Here comes the directory listing.
-rw-r--r-- 1 3003 65534 68940 Aug 28 1998 diction-0.7.tar.gz
-rw-r--r-- 1 3003 65534 90957 Mar 04 2002 diction-1.02.tar.gz
-rw-r--r-- 1 3003 65534 141062 Sep 17 2007 diction-1.11.tar.gz
-rw-r--r-- 1 3003 65534 189 Sep 17 2007 diction-1.11.tar.gz.sig
226 Directory send OK.
ftp> get diction-1.11.tar.gz
local: diction-1.11.tar.gz remote: diction-1.11.tar.gz
227 Entering Passive Mode (209,51,188,20,84,144).
150 Opening BINARY mode data connection for diction-1.11.tar.gz (141062 bytes).
226 Transfer complete.
141062 bytes received in 15.1 secs (9.32 Kbytes/sec)
ftp> bye
221 Goodbye.
[root@VM_0_7_centos src]# ls
diction-1.11.tar.gz
一旦 tar
文件下载下来之后,必须解包。通过 tar
程序可以完成:
[root@VM_0_7_centos src]# tar xzf diction-1.11.tar.gz
[root@VM_0_7_centos src]# ls
diction-1.11 diction-1.11.tar.gz
该 diction 程序,像所有的 GNU 项目软件,遵循着一定的源码打包标准。其它大多数在 Linux 生态系统中可用的源码也遵循这个标准。该标准的一个条目是,当源码 tar
文件打开的时候,会创建一个目录,该目录包含了源码树,并且这个目录将会命名为 project-x.xx
,其包含了项目名称和它的版本号两项内容。这种方案能在系统中方便安装同一程序的多个版本。然而,通常在打开 tarball 之前检验源码树的布局是个不错的主意。一些项目不会创建该目录,反而,会把文件直接传递给当前目录。这会把你的(除非组织良好的)src
目录弄得一片狼藉。为了避免这个,使用下面的命令,检查 tar
文件的内容:
tar tzvf tarfile | head ---
检查源码树
打开该 tar
文件,会创建一个新的目录,名为 diction-1.11。这个目录包含了源码树。让我们看一下里面的内容:
[root@VM_0_7_centos src]# cd diction-1.11/
[root@VM_0_7_centos diction-1.11]# ls
config.guess configure de diction.c diction.spec.in en_GB getopt.c INSTALL misc.c nl sentence.c style.c
config.h.in configure.in de.po diction.pot diction.texi.in en_GB.po getopt.h install-sh misc.h nl.po sentence.h test
config.sub COPYING diction.1.in diction.spec en getopt1.c getopt_int.h Makefile.in NEWS README style.1.in
这些文件包含了程序描述,如何建立和安装它的信息,还有其它许可条款。在试图建立程序之前,仔细阅读 README 和 INSTALL 文件,总是一个不错的主意。
在这个目录中,其它有趣的文件是那些以.c
和.h
为后缀的文件,这些.c
文件包含了由该软件包提供的两个 C 程序(style 和 diction),被分割成模块。这是一种常见做法,把大型程序分解成更小,更容易管理的代码块。源码文件都是普通文本,可以用 less
命令查看。这些.h
文件被称为头文件。它们也是普通文件。头文件包含了程序的描述,这些程序被包括在源码文件或库中。为了让编译器链接到模块,编译器必须接受所需的所有模块的描述,来完成整个程序。
构建程序
大多数程序通过一个简单的,两个命令的序列构建:
./configure
make
这个 configure 程序是一个 shell 脚本,由源码树提供。它的工作是分析程序构建环境。大多数源码会设计为可移植的。也就是说,它被设计成能够在不止一种类 Unix 系统中进行构建。但是为了做到这一点,在建立程序期间,为了适应系统之间的差异,源码可能需要经过轻微的调整。configure 也会检查是否安装了必要的外部工具和组件。让我们运行 configure 命令。因为 configure 命令所在的位置不是位于 shell 通常期望程序所呆的地方,我们必须明确地告诉shell 它的位置,通过在命令之前加上./
字符,来表明程序位于当前工作目录:
[root@VM_0_7_centos diction-1.11]# ./configure
configure 将会输出许多信息,随着它测试和配置整个构建过程。当结束后,输出结果看起来像这样:
checking build system type... x86_64-unknown-linux-gnu
checking host system type... x86_64-unknown-linux-gnu
checking for gcc... gcc
checking for C compiler default output file name... a.out
checking whether the C compiler works... yes
checking whether we are cross compiling... no
checking for suffix of executables...
checking for suffix of object files... o
checking whether we are using the GNU C compiler... yes
checking whether gcc accepts -g... yes
checking for gcc option to accept ISO C89... none needed
checking for a BSD-compatible install... /usr/bin/install -c
checking for strerror... yes
checking for library containing regcomp... none required
checking for broken realloc... no
checking for msgfmt... yes
checking how to run the C preprocessor... gcc -E
checking for grep that handles long lines and -e... /usr/bin/grep
checking for egrep... /usr/bin/grep -E
checking for ANSI C header files... yes
checking for sys/types.h... yes
checking for sys/stat.h... yes
checking for stdlib.h... yes
checking for string.h... yes
checking for memory.h... yes
checking for strings.h... yes
checking for inttypes.h... yes
checking for stdint.h... yes
checking for unistd.h... yes
checking libintl.h usability... yes
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
这里最重要的事情是没有错误信息。如果有错误信息,整个配置过程失败,然后程序不能构建直到修正了错误。
我们看到在我们的源码目录中 configure 命令创建了几个新文件。最重要一个是 Makefile。Makefile 是一个配置文件,指示 make 程序究竟如何构建程序。没有它,make 程序就不能运行。Makefile 是一个普通文本文件,所以我们能查看它。
这个 make 程序把一个 makefile 文件作为输入(通常命名为 Makefile),makefile 文件描述了包括最终完成的程序的各组件之间的关系和依赖性。
让我们运行 make
命令并构建我们的程序:
[root@VM_0_7_centos diction-1.11]# make
这个 make 程序将会运行,使用 Makefile 文件的内容来指导它的行为。它会产生很多信息。
当 make 程序运行结束后,现在我们将看到所有的目标文件出现在我们的目录中。
[root@VM_0_7_centos diction-1.11]# ls
config.guess config.sub de.mo diction.c diction.texi en_GB.po getopt_int.h Makefile.in nl sentence.h style.c
config.h configure de.po diction.o diction.texi.in getopt1.c getopt.o misc.c nl.mo sentence.o style.o
config.h.in configure.in diction diction.pot en getopt1.o INSTALL misc.h nl.po style test
config.log COPYING diction.1 diction.spec en_GB getopt.c install-sh misc.o README style.1
config.status de diction.1.in diction.spec.in en_GB.mo getopt.h Makefile NEWS sentence.c style.1.in
在这些文件之中,我们看到 diction 和 style,我们开始要构建的程序。恭喜一切正常!我们刚才源码编译了我们的第一个程序。但是出于好奇,让我们再运行一次 make 程序:
[root@VM_0_7_centos diction-1.11]# make
make: Nothing to be done for `all'.
它只是产生这样一条奇怪的信息。怎么了?为什么它没有重新构建程序呢?啊,这就是make 奇妙之处了。make 只是构建需要构建的部分,而不是简单地重新构建所有的内容。由于所有的目标文件都存在,make 确定没有任何事情需要做。我们可以证明这一点,通过删除一个目标文件,然后再次运行 make 程序,看看它做些什么。让我们去掉一个中间目标文件:
[root@VM_0_7_centos diction-1.11]# rm getopt.o
rm: remove regular file ‘getopt.o’? y
[root@VM_0_7_centos diction-1.11]# ls
config.guess config.sub de.mo diction.c diction.texi en_GB.po getopt_int.h misc.c nl.mo sentence.o style.o
config.h configure de.po diction.o diction.texi.in getopt1.c INSTALL misc.h nl.po style test
config.h.in configure.in diction diction.pot en getopt1.o install-sh misc.o README style.1
config.log COPYING diction.1 diction.spec en_GB getopt.c Makefile NEWS sentence.c style.1.in
config.status de diction.1.in diction.spec.in en_GB.mo getopt.h Makefile.in nl sentence.h style.c
[root@VM_0_7_centos diction-1.11]# make
gcc -c -I. -DSHAREDIR=\"/usr/local/share\" -DLOCALEDIR=\"/usr/local/share/locale\" -g -O2 -pipe -Wno-unused -Wshadow -Wbad-function-cast -Wmissing-prototypes -Wstrict-prototypes -Wcast-align -Wcast-qual -Wpointer-arith -Wcast-align -Wwrite-strings -Wmissing-declarations -Wnested-externs -Wundef -pedantic -fno-common getopt.c
gcc -o diction -g diction.o sentence.o misc.o \
getopt.o getopt1.o
gcc -o style -g style.o sentence.o misc.o \
getopt.o getopt1.o -lm
我们看到 make 重新构建了 getopt.o 文件,并重新链接了 diction 和 style 程序,因为它们依赖于丢失的模块。这种行为也指出了 make 程序的另一个重要特征:它保持目标文件是最新的。make 坚持目标文件要新于它们的依赖文件。
安装程序
打包良好的源码经常包括一个特别的 make 目标文件,叫做 install。这个目标文件将在系统目录中安装最终的产品,以供使用。通常,这个目录是 /usr/local/bin
,为在本地所构建软件的传统安装位置。然而,通常普通用户不能写入该目录,所以我们必须变成超级用户,来执行安装操作:
[root@VM_0_7_centos diction-1.11]# sudo make install
执行了安装后,我们可以检查下程序是否已经可用:
[root@VM_0_7_centos diction-1.11]# which diction
/usr/local/bin/diction
[root@VM_0_7_centos diction-1.11]# man diction
总结
在这一部分中,我们已经知道了三个简单命令:
./configure
make
make install
可以用来构建许多源码包。我们也知道了在程序维护过程中,make 程序起到了举足轻重的作用。make 程序可以用到任何需要维护一个目标/依赖关系的任务中,不仅仅为了编译源代码。