摘要:在这一章中,我们将看一下如何通过编译源代码来创建程序。make
编译程序
为什么要编译软件呢?有两个原因:
- 可用性。尽管系统发行版仓库中已经包含了大量的预编译程序,但是一些发行版本不可能包含所有期望的应用。在这种情况下,得到所期望程序的唯一方式是编译程序源码。
- 及时性。虽然一些系统发行版专门打包前沿版本的应用程序,但是很多不是。这意味着,为了拥有一个最新版本的程序,编译是必需的。
从源码编译软件可能非常复杂,但也可能只需要简单几步,这取决于程序包。我们将看一个简单案例,来从整体上认识一下编译过程。
我们将介绍一个新命令:
- make - 维护程序的工具
什么是编译
简单说,编译就是把源码翻译成计算机处理器的语言的过程。
机器语言只有0和1;汇编语言有了诸如CPY和MOV的助记符;高级语言就是我们学的C++、Java、C等。
用高级语言编写的程序,经过另一个称为编译器的程序的处理,会转换成机器语言。一些编译器把高级指令翻译成汇编语言,然后使用一个汇编器完成翻译成机器语言的最后阶段。一个称为链接的过程经常与编译结合在一起。一个叫做链接器的程序用来在编译器的输出结果和要编译的程序所需的库之间建立连接。
所有的程序都是可编译的吗?
不是。正如我们所看到的,有些程序比如 shell 脚本就不需要编译。它们直接执行。这些程序是用所谓的脚本或解释型语言编写的。
脚本语言由一个叫做解释器的特殊程序执行。一个解释器输入程序文件,读取并执行程序中包含的每一条指令。通常来说,解释型程序执行起来要比编译程序慢很多。这是因为每次解释型程序执行时,程序中每一条源码指令都需要翻译,而一个已经编译好的程序,一条源码指令只翻译了一次,翻译后的指令会永久地记录到最终的执行文件中。
程序开发需要经历一个不断重复的写码、编译和测试周期。随着程序变得越来越大,编译阶段会变得相当耗时。解释型语言删除了编译步骤,这样就加快了程序开发过程。
编译一个 C 语言
在编译之前,要确保系统安装了编译器、链接器以及make。
在 Linux 环境中,普遍使用的 C 编译器叫做 gcc (GNU C 编译器)。
得到源码
我们将编译一个叫做 diction 的程序,来自 GNU 项目。这是一个小巧方便的程序,检查文本文件的书写质量和样式。
首先我们要创建一个名为 src 的目录来存放我们的源码,然后使用 ftp 协议把源码下载下来。
xuxg@xuxg-ubuntu:~$ mkdir src
xuxg@xuxg-ubuntu:~$ cd src
xuxg@xuxg-ubuntu:~/src$ ftp ftp.gnu.org
Connected to ftp.gnu.org.
220 GNU FTP server ready.
Name (ftp.gnu.org:xuxg): 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 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
200 PORT command successful. Consider using PASV.
150 Opening BINARY mode data connection for diction-1.11.tar.gz (141062 bytes).
226 Transfer complete.
141062 bytes received in 0.92 secs (149.9320 kB/s)
ftp> bye
221 Goodbye.
xuxg@xuxg-ubuntu:~/src$ ls
diction-1.11.tar.gz
通常提供的源码形式是一个压缩的 tar 文件。有时候称为 tarball,这个文件包含源码树,或者是组成源码的目录和文件的层次结构。当到达 ftp 站点之后,我们检查可用的 tar 文件列表,然后选择最新版本,下载。使用 ftp 中的 get 命令,我们把文件从 ftp服务器复制到本地机器。
一旦 tar 文件下载下来之后,必须解包。通过 tar 程序可以完成:
xuxg@xuxg-ubuntu:~$ cd src
xuxg@xuxg-ubuntu:~/src$ tar xzf diction-1.11.tar.gz
xuxg@xuxg-ubuntu:~/src$ ls
diction-1.11 diction-1.11.tar.gz
检查源码树
打开该 tar 文件,会创建一个新的目录,名为 diction-1.11。这个目录包含了源码树。让我们看一下里面的内容:
xuxg@xuxg-ubuntu:~/src$ cd diction-1.11
xuxg@xuxg-ubuntu:~/src/diction-1.11$ ls
config.guess de diction.spec.in getopt.c misc.c sentence.c
config.h.in de.po diction.texi.in getopt.h misc.h sentence.h
config.sub diction.1.in en getopt_int.h NEWS style.1.in
configure diction.c en_GB INSTALL nl style.c
configure.in diction.pot en_GB.po install-sh nl.po test
COPYING diction.spec getopt1.c Makefile.in README
在源码树中,我们看到大量的文件。属于 GNU 项目的程序,还有其它许多程序都会,提供文档文件 README,INSTALL,NEWS,和 COPYING。这些文件包含了程序描述,如何建立和安装它的信息,还有其它许可条款。在试图建立程序之前,仔细阅读 README 和 INSTALL 文件,总是一个不错的主意。
那些以.c 和.h 为后缀的文件:
xuxg@xuxg-ubuntu:~/src/diction-1.11$ ls *.c
diction.c getopt1.c getopt.c misc.c sentence.c style.c
xuxg@xuxg-ubuntu:~/src/diction-1.11$ ls *.h
getopt.h getopt_int.h misc.h sentence.h
这些.c 文件包含了由该软件包提供的两个 C 程序(style 和 diction),被分割成模块。这是一种常见做法,把大型程序分解成更小,更容易管理的代码块。源码文件都是普通文本,可以用 less 命令查看:
xuxg@xuxg-ubuntu:~/src/diction-1.11$ less diction.c
这些.h 文件被称为头文件。它们也是普通文件。头文件包含了程序的描述,这些程序被包括在源码文件或库中。为了让编译器链接到模块,编译器必须接受所需的所有模块的描述,来完成整个程序。在 diction.c 文件的开头附近,我们看到这行代码:
#include "getopt.h"
当它读取 diction.c 中的源码的时候,这行代码指示编译器去读取文件 getopt.h,为的是“知道”getopt.c 中的内容。getopt.c 文件提供由 style 和 diction 两个程序共享的例行程序。
在 getopt.h 的 include 语句上面,我们看到一些其它的 include 语句,比如这些:
#include <regex.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
这些文件也是头文件,但是这些头文件在当前源码树的外面。它们由操作系统供给,来支持每个程序的编译。如果我们看一下 /usr/include 目录,能看到它们:
xuxg@xuxg-ubuntu:~/src/diction-1.11$ ls /usr/include
当我们安装编译器的时候,这个目录中的头文件会被安装。
构建程序
大多数程序通过一个简单的,两个命令的序列构建:
./configure
make
这个 configure 程序是一个 shell 脚本,由源码树提供。它的工作是分析程序构建环境。大多数源码会设计为可移植的。也就是说,它被设计成能够在不止一种类 Unix 系统中进行构建。但是为了做到这一点,在建立程序期间,为了适应系统之间的差异,源码可能需要经过轻微的调整。configure 也会检查是否安装了必要的外部工具和组件。让我们运行 configure 命令。因为 configure 命令所在的位置不是位于 shell 通常期望程序所呆的地方,我们必须明确地告诉shell 它的位置,通过在命令之前加上./ 字符,来表明程序位于当前工作目录:
xuxg@xuxg-ubuntu:~/src/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... /bin/grep
checking for egrep... /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 是一个普通文本文件,所以我们能查看它:
xuxg@xuxg-ubuntu:~/src/diction-1.11$ less Makefile
这个 make 程序把一个 makefile 文件作为输入(通常命名为 Makefile),makefile 文件描述了包括最终完成的程序的各组件之间的关系和依赖性。
makefile 文件的第一部分定义了变量,这些变量在该 makefile 后续章节中会被替换掉。例如我们看看这一行代码:
CC= gcc
其定义了所用的 C 编译器是 gcc。文件后面部分,我们看到一个使用该变量的实例:
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 文件由行组成,每行定义一个目标文件,在这种情况下,目标文件是指可执行文件 diction,还有目标文件所依赖的文件。剩下的行描述了从目标文件的依赖组件中创建目标文件所需的命令。在这个例子中,我们看到可执行文件 diction(最终的成品之一)依赖于文件 diction.o,sentence.o,misc.o,getopt.o,和 getopt1.o 都存在。在 makefile 文件后面部分,我们看到 diction 文件所依赖的每一个文件做为目标文件的定义:
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 文件:
.c.o:
$(CC) -c $(CPPFLAGS) $(CFLAGS) $<
这些看起来非常复杂。为什么不简单地列出编译每个部分的步骤,那样不就行了?一会儿就知道答案了。同时,让我们运行 make 命令并构建我们的程序:
xuxg@xuxg-ubuntu:~/src/diction-1.11$ make
这个 make 程序将会运行,使用 Makefile 文件的内容来指导它的行为。它会产生很多信息。当 make 程序运行结束后,现在我们将看到所有的目标文件出现在我们的目录中。
xuxg@xuxg-ubuntu:~/src/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,我们开始要构建的程序。恭喜一切正常!我们刚才源码编译了我们的第一个程序。但是出于好奇,让我们再运行一次 make 程序:
xuxg@xuxg-ubuntu:~/src/diction-1.11$ make
make: Nothing to be done for 'all'.
它只是产生这样一条奇怪的信息。怎么了?为什么它没有重新构建程序呢?啊,这就是make 奇妙之处了。make 只是构建需要构建的部分,而不是简单地重新构建所有的内容。由于所有的目标文件都存在,make 确定没有任何事情需要做。我们可以证明这一点,通过删除一个目标文件,然后再次运行 make 程序,看看它做些什么。让我们去掉一个中间目标文件:
xuxg@xuxg-ubuntu:~/src/diction-1.11$ rm getopt.o
xuxg@xuxg-ubuntu:~/src/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 来构建一个新版本的成品。make 确保基于更新的代码构建了需要构建的内容。如果我们使用 touch 程序,来“更新”其中一个源码文件,我们看到发生了这样的事情:
xuxg@xuxg-ubuntu:~/src/diction-1.11$ ls -l diction getopt.c
-rwxr-xr-x 1 xuxg xuxg 62320 Mar 28 10:58 diction
-rw-r--r-- 1 xuxg xuxg 33125 Mar 31 2007 getopt.c
xuxg@xuxg-ubuntu:~/src/diction-1.11$ touch getopt.c
xuxg@xuxg-ubuntu:~/src/diction-1.11$ ls -l diction getopt.c
-rwxr-xr-x 1 xuxg xuxg 62320 Mar 28 10:58 diction
-rw-r--r-- 1 xuxg xuxg 33125 Mar 28 11:00 getopt.c
xuxg@xuxg-ubuntu:~/src/diction-1.11$ make
运行 make 之后,我们看到目标文件已经更新于它的依赖文件:
xuxg@xuxg-ubuntu:~/src/diction-1.11$ ls -l diction getopt.c
-rwxr-xr-x 1 xuxg xuxg 62320 Mar 28 11:01 diction
-rw-r--r-- 1 xuxg xuxg 33125 Mar 28 11:00 getopt.c
make 程序这种智能地只构建所需要构建的内容的特性,对程序来说,是巨大的福利。虽然在我们的小项目中,节省的时间可能不是非常明显,在庞大的工程中,它具有非常重大的意义。记住,Linux 内核(一个经历着不断修改和改进的程序)包含了几百万行代码。
安装程序
打包良好的源码经常包括一个特别的 make 目标文件,叫做 install。这个目标文件将在系统目录中安装最终的产品,以供使用。通常,这个目录是 /usr/local/bin,为在本地所构建软件的传统安装位置。然而,通常普通用户不能写入该目录,所以我们必须变成超级用户,来执行安装操作:
xuxg@xuxg-ubuntu:~/src/diction-1.11$ sudo make install
执行了安装后,我们可以检查下程序是否已经可用:
xuxg@xuxg-ubuntu:~/src/diction-1.11$ which diction
/usr/local/bin/diction
xuxg@xuxg-ubuntu:~/src/diction-1.11$ man diction
完美!
总结
在这一章中,我们已经知道了三个简单命令:
./configure
make
make install
可以用来构建许多源码包。我们也知道了在程序维护过程中,make 程序起到了举足轻重的作用。make 程序可以用到任何需要维护一个目标/依赖关系的任务中,不仅仅为了编译源代码。
写到这里,TLCL这本书的第三部分就结束了,下周开始最后一部分——编写shell脚本。最近其实心情不太好,老师分配了新任务,我哪会solidworks啊,当然,后边敲程序估计我也不会。慢慢学吧。
美国确诊新冠肺炎超10万例了,要进入四月份了,感觉最多一个月就要开学了。
加油,还是要好好努力,积极面对生活中的苦与乐,赶紧成长起来吧!