在這一章中,我們將看一下如何通過編譯源代碼來創(chuàng)建程序。源代碼的可用性是至關(guān)重要的自由,從而使得 Linux 成為可能。 整個(gè) Linux 開發(fā)生態(tài)圈就是依賴于開發(fā)者之間的自由交流。對(duì)于許多桌面用戶來說,編譯是一種失傳的藝術(shù)。以前很常見, 但現(xiàn)在,由系統(tǒng)發(fā)行版提供商維護(hù)巨大的預(yù)編譯的二進(jìn)制倉庫,準(zhǔn)備供用戶下載和使用。在寫這篇文章的時(shí)候, Debian 倉庫(最大的發(fā)行版之一)包含了幾乎23,000個(gè)預(yù)編譯的包。
那么為什么要編譯軟件呢? 有兩個(gè)原因:
可用性。盡管系統(tǒng)發(fā)行版?zhèn)}庫中已經(jīng)包含了大量的預(yù)編譯程序,但是一些發(fā)行版本不可能包含所有期望的應(yīng)用。 在這種情況下,得到所期望程序的唯一方式是編譯程序源碼。
從源碼編譯軟件可以變得非常復(fù)雜且具有技術(shù)性;許多用戶難以企及。然而,許多編譯任務(wù)是 相當(dāng)簡單的,只涉及到幾個(gè)步驟。這都取決于程序包。我們將看一個(gè)非常簡單的案例, 為的是給大家提供一個(gè)對(duì)編譯過程的整體認(rèn)識(shí),并為那些愿意進(jìn)一步學(xué)習(xí)的人們構(gòu)筑一個(gè)起點(diǎn)。
我們將介紹一個(gè)新命令:
簡而言之,編譯就是把源碼(一個(gè)由程序員編寫的人類可讀的程序描述)翻譯成計(jì)算機(jī)處理器的母語的過程。
計(jì)算機(jī)處理器(或 CPU)工作在一個(gè)非常基本的水平,執(zhí)行用機(jī)器語言編寫的程序。這是一種數(shù)值編碼,描述非常小的操作, 比如“加這個(gè)字節(jié)”,“指向內(nèi)存中的這個(gè)位置”,或者“復(fù)制這個(gè)字節(jié)”。
這些指令中的每一條都是用二進(jìn)制表示的(1和0)。最早的計(jì)算機(jī)程序就是用這種數(shù)值編碼寫成的,這可能就 解釋了為什么編寫它們的程序員據(jù)說吸很多煙,喝大量咖啡,并帶著厚厚的眼鏡。這個(gè)問題克服了,隨著匯編語言的出現(xiàn), 匯編語言代替了數(shù)值編碼(略微)簡便地使用助記符,比如 CPY(復(fù)制)和 MOV(移動(dòng))。用匯編語言編寫的程序通過 匯編器處理為機(jī)器語言。今天為了完成某些特定的程序任務(wù),匯編語言仍在被使用,例如設(shè)備驅(qū)動(dòng)和嵌入式系統(tǒng)。
下一步我們談?wù)撘幌率裁词撬^的高級(jí)編程語言。之所以這樣稱呼它們,是因?yàn)樗鼈兛梢宰尦绦騿T少操心處理器的 一舉一動(dòng),而更多關(guān)心如何解決手頭的問題。早期的高級(jí)語言(二十世紀(jì)60年代期間研發(fā)的)包括 FORTRAN(為科學(xué)和技術(shù)問題而設(shè)計(jì))和 COBOL(為商業(yè)應(yīng)用而設(shè)計(jì))。今天這兩種語言仍在有限的使用。
雖然有許多流行的編程語言,兩個(gè)占主導(dǎo)地位。大多數(shù)為現(xiàn)代系統(tǒng)編寫的程序,要么用 C 編寫,要么是用 C++ 編寫。 在隨后的例子中,我們將編寫一個(gè) C 程序。
用高級(jí)語言編寫的程序,經(jīng)過另一個(gè)稱為編譯器的程序的處理,會(huì)轉(zhuǎn)換成機(jī)器語言。一些編譯器把 高級(jí)指令翻譯成匯編語言,然后使用一個(gè)匯編器完成翻譯成機(jī)器語言的最后階段。
一個(gè)稱為鏈接的過程經(jīng)常與編譯結(jié)合在一起。有許多程序執(zhí)行的常見任務(wù)。以打開文件為例。許多程序執(zhí)行這個(gè)任務(wù), 但是讓每個(gè)程序?qū)崿F(xiàn)它自己的打開文件功能,是很浪費(fèi)資源的。更有意義的是,擁有單獨(dú)的一段知道如何打開文件的程序, 并允許所有需要它的程序共享它。對(duì)常見任務(wù)提供支持由所謂的庫完成。這些庫包含多個(gè)程序,每個(gè)程序執(zhí)行 一些可以由多個(gè)程序共享的常見任務(wù)。如果我們看一下 /lib 和 /usr/lib 目錄,我們可以看到許多庫定居在那里。 一個(gè)叫做鏈接器的程序用來在編譯器的輸出結(jié)果和要編譯的程序所需的庫之間建立連接。這個(gè)過程的最終結(jié)果是 一個(gè)可執(zhí)行程序文件,準(zhǔn)備使用。
不是。正如我們所看到的,有些程序比如 shell 腳本就不需要編譯。它們直接執(zhí)行。 這些程序是用所謂的腳本或解釋型語言編寫的。近年來,這些語言變得越來越流行,包括 Perl, Python,PHP,Ruby,和許多其它語言。
腳本語言由一個(gè)叫做解釋器的特殊程序執(zhí)行。一個(gè)解釋器輸入程序文件,讀取并執(zhí)行程序中包含的每一條指令。 通常來說,解釋型程序執(zhí)行起來要比編譯程序慢很多。這是因?yàn)槊看谓忉屝统绦驁?zhí)行時(shí),程序中每一條源碼指令都需要翻譯, 而一個(gè)編譯程序,一條源碼指令只翻譯一次,翻譯后的指令會(huì)永久地記錄到最終的執(zhí)行文件中。
那么為什么解釋型程序這樣流行呢?對(duì)于許多編程任務(wù)來說,原因是“足夠快”,但是真正的優(yōu)勢(shì)是一般來說開發(fā)解釋型程序 要比編譯程序快速且容易。通常程序開發(fā)需要經(jīng)歷一個(gè)不斷重復(fù)的寫碼,編譯,測(cè)試周期。隨著程序變得越來越大, 編譯階段會(huì)變得相當(dāng)耗時(shí)。解釋型語言刪除了編譯步驟,這樣就加快了程序開發(fā)。
讓我們編譯一些東西。在我們行動(dòng)之前,然而我們需要一些工具,像編譯器,鏈接器,還有 make。 在 Linux 環(huán)境中,普遍使用的 C 編譯器叫做 gcc(GNU C 編譯器),最初由 Richard Stallman 寫出來的。 大多數(shù) Linux 系統(tǒng)發(fā)行版默認(rèn)不安裝 gcc。我們可以這樣查看該編譯器是否存在:
[me@linuxbox ~]$ which gcc
/usr/bin/gcc
在這個(gè)例子中的輸出結(jié)果表明安裝了 gcc 編譯器。
為了我們的編譯練習(xí),我們將編譯一個(gè)叫做 diction 的程序,來自 GNU 項(xiàng)目。這是一個(gè)小巧方便的程序, 檢查文本文件的書寫質(zhì)量和樣式。就程序而言,它相當(dāng)小,且容易創(chuàng)建。
遵照慣例,首先我們要?jiǎng)?chuàng)建一個(gè)名為 src 的目錄來存放我們的源碼,然后使用 ftp 協(xié)議把源碼下載下來。
[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
注意:因?yàn)槲覀兪沁@個(gè)源碼的“維護(hù)者”,當(dāng)我們編譯它的時(shí)候,我們把它保存在 ~/src 目錄下。 由你的系統(tǒng)發(fā)行版源碼會(huì)把源碼安裝在 /usr/src 目錄下,而供多個(gè)用戶使用的源碼,通常安裝在 /usr/local/src 目錄下。
正如我們所看到的,通常提供的源碼形式是一個(gè)壓縮的 tar 文件。有時(shí)候稱為 tarball,這個(gè)文件包含源碼樹, 或者是組成源碼的目錄和文件的層次結(jié)構(gòu)。當(dāng)?shù)竭_(dá) ftp 站點(diǎn)之后,我們檢查可用的 tar 文件列表,然后選擇最新版本,下載。 使用 ftp 中的 get 命令,我們把文件從 ftp 服務(wù)器復(fù)制到本地機(jī)器。
一旦 tar 文件下載下來之后,必須打開。通過 tar 程序可以完成:
[me@linuxbox src]$ tar xzf diction-1.11.tar.gz
[me@linuxbox src]$ ls
diction-1.11
diction-1.11.tar.gz
小提示:該 diction 程序,像所有的 GNU 項(xiàng)目軟件,遵循著一定的源碼打包標(biāo)準(zhǔn)。其它大多數(shù)在 Linux 生態(tài)系統(tǒng)中 可用的源碼也遵循這個(gè)標(biāo)準(zhǔn)。該標(biāo)準(zhǔn)的一個(gè)條目是,當(dāng)源碼 tar 文件打開的時(shí)候,會(huì)創(chuàng)建一個(gè)目錄,該目錄包含了源碼樹, 并且這個(gè)目錄將會(huì)命名為 project-x.xx,其包含了項(xiàng)目名稱和它的版本號(hào)兩項(xiàng)內(nèi)容。這種方案能在系統(tǒng)中方便安裝同一程序的多個(gè)版本。 然而,通常在打開 tarball 之前檢驗(yàn)源碼樹的布局是個(gè)不錯(cuò)的主意。一些項(xiàng)目不會(huì)創(chuàng)建該目錄,反而,會(huì)把文件直接傳遞給當(dāng)前目錄。 這會(huì)把你的(除非組織良好的)src 目錄弄得一片狼藉。為了避免這個(gè),使用下面的命令,檢查 tar 文件的內(nèi)容:
tar tzvf tarfile | head
打開該 tar 文件,會(huì)創(chuàng)建一個(gè)新的目錄,名為 diction-1.11。這個(gè)目錄包含了源碼樹。讓我們看一下里面的內(nèi)容:
[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 項(xiàng)目的程序,還有其它許多程序都會(huì),提供文檔文件 README,INSTALL,NEWS,和 COPYING。
這些文件包含了程序描述,如何建立和安裝它的信息,還有它許可條款。在試圖建立程序之前,仔細(xì)閱讀 README 和 INSTALL 文件,總是一個(gè)不錯(cuò)的主意。
在這個(gè)目錄中,其它有趣的文件是那些以 .c 和 .h 為后綴的文件:
[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 文件包含了由該軟件包提供的兩個(gè) C 程序(style 和 diction),被分割成模塊。這是一種常見做法,把大型程序 分解成更小,更容易管理的代碼塊。源碼文件都是普通文本,可以用 less 命令查看:
[me@linuxbox diction-1.11]$ less diction.c
這些 .h 文件以頭文件而著稱。它們也是普通文件。頭文件包含了程序的描述,這些程序被包括在源碼文件或庫中。 為了讓編譯器鏈接到模塊,編譯器必須接受所需的所有模塊的描述,來完成整個(gè)程序。在 diction.c 文件的開頭附近, 我們看到這行代碼:
#include "getopt.h"
這行代碼指示編譯器去讀取文件 getopt.h,因?yàn)樗鼤?huì)讀取 diction.c 中的源碼,為的是“知道” getopt.c 中的內(nèi)容。 getopt.c 文件提供由 style 和 diction 兩個(gè)程序共享的代碼。
在 getopt.h 的 include 語句上面,我們看到一些其它的 include 語句,比如這些:
#include <regex.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
這些也涉及到頭文件,但是這些頭文件居住在當(dāng)前源碼樹的外面。它們由操作系統(tǒng)供給,來支持每個(gè)程序的編譯。 如果我們看一下 /usr/include 目錄,能看到它們:
[me@linuxbox diction-1.11]$ ls /usr/include
當(dāng)我們安裝編譯器的時(shí)候,這個(gè)目錄中的頭文件會(huì)被安裝。
大多數(shù)程序通過一個(gè)簡單的,兩個(gè)命令的序列構(gòu)建:
./configure
make
這個(gè) configure 程序是一個(gè) shell 腳本,由源碼樹提供。它的工作是分析程序建立環(huán)境。大多數(shù)源碼會(huì)設(shè)計(jì)為可移植的。 也就是說,它被設(shè)計(jì)成,能建立在多于一個(gè)的類 Unix 系統(tǒng)中。但是為了做到這一點(diǎn),在建立程序期間,為了適應(yīng)系統(tǒng)之間的差異, 源碼可能需要經(jīng)過輕微的調(diào)整。configure 也會(huì)檢查是否安裝了必要的外部工具和組件。讓我們運(yùn)行 configure 命令。 因?yàn)?configure 命令所在的位置不是位于 shell 通常期望程序所呆的地方,我們必須明確地告訴 shell 它的位置,通過 在命令之前加上 ./ 字符,來表明程序位于當(dāng)前工作目錄:
[me@linuxbox diction-1.11]$ ./configure
configure 將會(huì)輸出許多信息,隨著它測(cè)試和配置整個(gè)構(gòu)建過程。當(dāng)結(jié)束后,輸出結(jié)果看起來像這樣:
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]$
這里最重要的事情是沒有錯(cuò)誤信息。如果有錯(cuò)誤信息,整個(gè)配置過程失敗,然后程序不能構(gòu)建直到修正了錯(cuò)誤。
我們看到在我們的源碼目錄中 configure 命令創(chuàng)建了幾個(gè)新文件。最重要一個(gè)是 Makefile。Makefile 是一個(gè)配置文件, 指示 make 程序究竟如何構(gòu)建程序。沒有它,make 程序就不能運(yùn)行。Makefile 是一個(gè)普通文本文件,所以我們能查看它:
[me@linuxbox diction-1.11]$ less Makefile
這個(gè) make 程序把一個(gè) makefile 文件作為輸入(通常命名為 Makefile),makefile 文件 描述了包括最終完成的程序的各組件之間的關(guān)系和依賴性。
makefile 文件的第一部分定義了變量,這些變量在該 makefile 后續(xù)章節(jié)中會(huì)被替換掉。例如我們看看這一行代碼:
CC= gcc
其定義了所用的 C 編譯器是 gcc。文件后面部分,我們看到一個(gè)使用該變量的實(shí)例:
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)
這里完成了一個(gè)替換操作,在程序運(yùn)行時(shí),$(CC) 的值會(huì)被替換成 gcc。大多數(shù) makefile 文件由行組成,每行定義一個(gè)目標(biāo)文件, 在這種情況下,目標(biāo)文件是指可執(zhí)行文件 diction,還有目標(biāo)文件所依賴的文件。剩下的行描述了從目標(biāo)文件的依賴組件中 創(chuàng)建目標(biāo)文件所需的命令。在這個(gè)例子中,我們看到可執(zhí)行文件 diction(最終的成品之一)依賴于文件 diction.o,sentence.o,misc.o,getopt.o,和 getopt1.o都存在。在 makefile 文件后面部分,我們看到 diction 文件所依賴的每一個(gè)文件做為目標(biāo)文件的定義:
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
然而,我們不會(huì)看到針對(duì)它們的任何命令。這個(gè)由一個(gè)通用目標(biāo)解決,在文件的前面,描述了這個(gè)命令,用來把任意的 .c 文件編譯成 .o 文件:
.c.o:
$(CC) -c $(CPPFLAGS) $(CFLAGS) $<
這些看起來非常復(fù)雜。為什么不簡單地列出所有的步驟,編譯完成每一部分?一會(huì)兒就知道答案了。同時(shí), 讓我們運(yùn)行 make 命令并構(gòu)建我們的程序:
[me@linuxbox diction-1.11]$ make
這個(gè) make 程序?qū)?huì)運(yùn)行,使用 Makefile 文件的內(nèi)容來指導(dǎo)它的行為。它會(huì)產(chǎn)生很多信息。
當(dāng) make 程序運(yùn)行結(jié)束后,現(xiàn)在我們將看到所有的目標(biāo)文件出現(xiàn)在我們的目錄中。
[me@linuxbox diction-1.11]$ ls
config.guess de.po en en_GB sentence.c
config.h diction en_GB.mo en_GB.po sentence.h
config.h.in diction.1 getopt1.c getopt1.o sentence.o
config.log diction.1.in getopt.c getopt.h style
config.status diction.c getopt_int.h getopt.o style.1
config.sub diction.o INSTALL install-sh style.1.in
configure diction.pot Makefile Makefile.in style.c
configure.in diction.spec misc.c misc.h style.o
COPYING diction.spec.in misc.o NEWS test
de diction.texi nl nl.mo
de.mo diction.texi.i nl.po README
在這些文件之中,我們看到 diction 和 style,我們開始要構(gòu)建的程序。恭喜一切正常!我們剛才源碼編譯了 我們的第一個(gè)程序。但是出于好奇,讓我們?cè)龠\(yùn)行一次 make 程序:
[me@linuxbox diction-1.11]$ make
make: Nothing to be done for `all'.
它只是產(chǎn)生這樣一條奇怪的信息。怎么了?為什么它沒有重新構(gòu)建程序呢?啊,這就是 make 奇妙之處了。make 只是構(gòu)建 需要構(gòu)建的部分,而不是簡單地重新構(gòu)建所有的內(nèi)容。由于所有的目標(biāo)文件都存在,make 確定沒有任何事情需要做。 我們可以證明這一點(diǎn),通過刪除一個(gè)目標(biāo)文件,然后再次運(yùn)行 make 程序,看看它做些什么。讓我們?nèi)サ粢粋€(gè)中間目標(biāo)文件:
[me@linuxbox diction-1.11]$ rm getopt.o
[me@linuxbox diction-1.11]$ make
我們看到 make 重新構(gòu)建了 getopt.o 文件,并重新鏈接了 diction 和 style 程序,因?yàn)樗鼈円蕾囉趤G失的模塊。 這種行為也指出了 make 程序的另一個(gè)重要特征:它保持目標(biāo)文件是最新的。make 堅(jiān)持目標(biāo)文件要新于它們的依賴文件。 這個(gè)非常有意義,做為一名程序員,經(jīng)常會(huì)更新一點(diǎn)兒源碼,然后使用 make 來構(gòu)建一個(gè)新版本的成品。make 確保 基于更新的代碼構(gòu)建了需要構(gòu)建的內(nèi)容。如果我們使用 touch 程序,來“更新”其中一個(gè)源碼文件,我們看到發(fā)生了這樣的事情:
[me@linuxboxdiction-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@linuxboxdiction-1.11]$ touch getopt.c
[me@linuxboxdiction-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
運(yùn)行 make 之后,我們看到目標(biāo)文件已經(jīng)更新于它的依賴文件:
[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 程序這種智能地只構(gòu)建所需要構(gòu)建的內(nèi)容的特性,對(duì)程序來說,是巨大的福利。雖然在我們的小項(xiàng)目中,節(jié)省的時(shí)間可能 不是非常明顯,在龐大的工程中,它具有非常重大的意義。記住,Linux 內(nèi)核(一個(gè)經(jīng)歷著不斷修改和改進(jìn)的程序)包含了幾百萬行代碼。
打包良好的源碼經(jīng)常包括一個(gè)特別的 make 目標(biāo)文件,叫做 install。這個(gè)目標(biāo)文件將在系統(tǒng)目錄中安裝最終的產(chǎn)品,以供使用。 通常,這個(gè)目錄是 /usr/local/bin,為在本地所構(gòu)建軟件的傳統(tǒng)安裝位置。然而,通常普通用戶不能寫入該目錄,所以我們必須變成超級(jí)用戶, 來執(zhí)行安裝操作:
[me@linuxbox diction-1.11]$ sudo make install
After we perform the installation, we can check that the program is ready to go:
[me@linuxbox diction-1.11]$ which diction
/usr/local/bin/diction
[me@linuxbox diction-1.11]$ man diction
And there we have it!
在這一章中,我們已經(jīng)知道了三個(gè)簡單命令:
./configure
make
make install
可以用來構(gòu)建許多源碼包。我們也知道了在程序維護(hù)過程中,make 程序起到了舉足輕重的作用。make 程序可以用到 任何需要維護(hù)一個(gè)目標(biāo)/依賴關(guān)系的任務(wù)中,不僅僅為了編譯源代碼。
Wikipedia 上面有關(guān)于編譯器和 make 程序的好文章:
GNU Make 手冊(cè)
http://www.gnu.org/software/make/manual/html_node/index.html