在這一章中,我們將看一下如何通過編譯源代碼來創建程序。源代碼的可用性是至關重要的自由,從而使得 Linux 成為可能。 整個 Linux 開發生態圈就是依賴于開發者之間的自由交流。對于許多桌面用戶來說,編譯是一種失傳的藝術。以前很常見, 但現在,由系統發行版提供商維護巨大的預編譯的二進制倉庫,準備供用戶下載和使用。在寫這篇文章的時候, Debian 倉庫(最大的發行版之一)包含了幾乎23,000個預編譯的包。
那么為什么要編譯軟件呢? 有兩個原因:
1. 可用性。盡管系統發行版倉庫中已經包含了大量的預編譯程序,但是一些發行版本不可能包含所有期望的應用。 在這種情況下,得到所期望程序的唯一方式是編譯程序源碼。
2. 及時性。雖然一些系統發行版專門打包前沿版本的應用程序,但是很多不是。這意味著, 為了擁有一個最新版本的程序,編譯是必需的。
從源碼編譯軟件可以變得非常復雜且具有技術性;許多用戶難以企及。然而,許多編譯任務是 相當簡單的,只涉及到幾個步驟。這都取決于程序包。我們將看一個非常簡單的案例, 為的是給大家提供一個對編譯過程的整體認識,并為那些愿意進一步學習的人們構筑一個起點。
我們將介紹一個新命令:
> * make - 維護程序的工具
## 什么是編譯?
簡而言之,編譯就是把源碼(一個由程序員編寫的人類可讀的程序描述)翻譯成計算機處理器的母語的過程。
計算機處理器(或 CPU)工作在一個非常基本的水平,執行用機器語言編寫的程序。這是一種數值編碼,描述非常小的操作, 比如“加這個字節”,“指向內存中的這個位置”,或者“復制這個字節”。
這些指令中的每一條都是用二進制表示的(1和0)。最早的計算機程序就是用這種數值編碼寫成的,這可能就 解釋了為什么編寫它們的程序員據說吸很多煙,喝大量咖啡,并帶著厚厚的眼鏡。這個問題克服了,隨著匯編語言的出現, 匯編語言代替了數值編碼(略微)簡便地使用助記符,比如 CPY(復制)和 MOV(移動)。用匯編語言編寫的程序通過 匯編器處理為機器語言。今天為了完成某些特定的程序任務,匯編語言仍在被使用,例如設備驅動和嵌入式系統。
下一步我們談論一下什么是所謂的高級編程語言。之所以這樣稱呼它們,是因為它們可以讓程序員少操心處理器的 一舉一動,而更多關心如何解決手頭的問題。早期的高級語言(二十世紀60年代期間研發的)包括 FORTRAN(為科學和技術問題而設計)和 COBOL(為商業應用而設計)。今天這兩種語言仍在有限的使用。
雖然有許多流行的編程語言,兩個占主導地位。大多數為現代系統編寫的程序,要么用 C 編寫,要么是用 C++ 編寫。 在隨后的例子中,我們將編寫一個 C 程序。
用高級語言編寫的程序,經過另一個稱為編譯器的程序的處理,會轉換成機器語言。一些編譯器把 高級指令翻譯成匯編語言,然后使用一個匯編器完成翻譯成機器語言的最后階段。
一個稱為鏈接的過程經常與編譯結合在一起。有許多程序執行的常見任務。以打開文件為例。許多程序執行這個任務, 但是讓每個程序實現它自己的打開文件功能,是很浪費資源的。更有意義的是,擁有單獨的一段知道如何打開文件的程序, 并允許所有需要它的程序共享它。對常見任務提供支持由所謂的庫完成。這些庫包含多個程序,每個程序執行 一些可以由多個程序共享的常見任務。如果我們看一下 /lib 和 /usr/lib 目錄,我們可以看到許多庫定居在那里。 一個叫做鏈接器的程序用來在編譯器的輸出結果和要編譯的程序所需的庫之間建立連接。這個過程的最終結果是 一個可執行程序文件,準備使用。
### 所有的程序都是可編譯的嗎?
不是。正如我們所看到的,有些程序比如 shell 腳本就不需要編譯。它們直接執行。 這些程序是用所謂的腳本或解釋型語言編寫的。近年來,這些語言變得越來越流行,包括 Perl, Python,PHP,Ruby,和許多其它語言。
腳本語言由一個叫做解釋器的特殊程序執行。一個解釋器輸入程序文件,讀取并執行程序中包含的每一條指令。 通常來說,解釋型程序執行起來要比編譯程序慢很多。這是因為每次解釋型程序執行時,程序中每一條源碼指令都需要翻譯, 而一個編譯程序,一條源碼指令只翻譯一次,翻譯后的指令會永久地記錄到最終的執行文件中。
那么為什么解釋型程序這樣流行呢?對于許多編程任務來說,原因是“足夠快”,但是真正的優勢是一般來說開發解釋型程序 要比編譯程序快速且容易。通常程序開發需要經歷一個不斷重復的寫碼,編譯,測試周期。隨著程序變得越來越大, 編譯階段會變得相當耗時。解釋型語言刪除了編譯步驟,這樣就加快了程序開發。
## 編譯一個 C 語言
讓我們編譯一些東西。在我們行動之前,然而我們需要一些工具,像編譯器,鏈接器,還有 make。 在 Linux 環境中,普遍使用的 C 編譯器叫做 gcc(GNU C 編譯器),最初由 Richard Stallman 寫出來的。 大多數 Linux 系統發行版默認不安裝 gcc。我們可以這樣查看該編譯器是否存在:
~~~
[me@linuxbox ~]$ which gcc
/usr/bin/gcc
~~~
在這個例子中的輸出結果表明安裝了 gcc 編譯器。
* * *
小提示: 你的系統發行版可能有一個用于軟件開發的 meta-package(軟件包的集合)。如果是這樣的話, 考慮安裝它,若你打算在你的系統中編譯程序。若你的系統沒有提供一個 meta-package,試著安裝 gcc 和 make 工具包。 在許多發行版中,這就足夠完成下面的練習了。 —
### 得到源碼
為了我們的編譯練習,我們將編譯一個叫做 diction 的程序,來自 GNU 項目。這是一個小巧方便的程序, 檢查文本文件的書寫質量和樣式。就程序而言,它相當小,且容易創建。
遵照慣例,首先我們要創建一個名為 src 的目錄來存放我們的源碼,然后使用 ftp 協議把源碼下載下來。
~~~
[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
~~~
* * *
注意:因為我們是這個源碼的“維護者”,當我們編譯它的時候,我們把它保存在 ~/src 目錄下。 由你的系統發行版源碼會把源碼安裝在 /usr/src 目錄下,而供多個用戶使用的源碼,通常安裝在 /usr/local/src 目錄下。
* * *
正如我們所看到的,通常提供的源碼形式是一個壓縮的 tar 文件。有時候稱為 tarball,這個文件包含源碼樹, 或者是組成源碼的目錄和文件的層次結構。當到達 ftp 站點之后,我們檢查可用的 tar 文件列表,然后選擇最新版本,下載。 使用 ftp 中的 get 命令,我們把文件從 ftp 服務器復制到本地機器。
一旦 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 項目軟件,遵循著一定的源碼打包標準。其它大多數在 Linux 生態系統中 可用的源碼也遵循這個標準。該標準的一個條目是,當源碼 tar 文件打開的時候,會創建一個目錄,該目錄包含了源碼樹, 并且這個目錄將會命名為 project-x.xx,其包含了項目名稱和它的版本號兩項內容。這種方案能在系統中方便安裝同一程序的多個版本。 然而,通常在打開 tarball 之前檢驗源碼樹的布局是個不錯的主意。一些項目不會創建該目錄,反而,會把文件直接傳遞給當前目錄。 這會把你的(除非組織良好的)src 目錄弄得一片狼藉。為了避免這個,使用下面的命令,檢查 tar 文件的內容:
~~~
tar tzvf tarfile | head ---
~~~
## 檢查源碼樹
打開該 tar 文件,會創建一個新的目錄,名為 diction-1.11。這個目錄包含了源碼樹。讓我們看一下里面的內容:
~~~
[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 為后綴的文件:
~~~
[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 命令查看:
~~~
[me@linuxbox diction-1.11]$ less diction.c
~~~
這些 .h 文件以頭文件而著稱。它們也是普通文件。頭文件包含了程序的描述,這些程序被包括在源碼文件或庫中。 為了讓編譯器鏈接到模塊,編譯器必須接受所需的所有模塊的描述,來完成整個程序。在 diction.c 文件的開頭附近, 我們看到這行代碼:
~~~
#include "getopt.h"
~~~
這行代碼指示編譯器去讀取文件 getopt.h,因為它會讀取 diction.c 中的源碼,為的是“知道” 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 目錄,能看到它們:
~~~
[me@linuxbox diction-1.11]$ ls /usr/include
~~~
當我們安裝編譯器的時候,這個目錄中的頭文件會被安裝。
### 構建程序
大多數程序通過一個簡單的,兩個命令的序列構建:
~~~
./configure
make
~~~
這個 configure 程序是一個 shell 腳本,由源碼樹提供。它的工作是分析程序建立環境。大多數源碼會設計為可移植的。 也就是說,它被設計成,能建立在多于一個的類 Unix 系統中。但是為了做到這一點,在建立程序期間,為了適應系統之間的差異, 源碼可能需要經過輕微的調整。configure 也會檢查是否安裝了必要的外部工具和組件。讓我們運行 configure 命令。 因為 configure 命令所在的位置不是位于 shell 通常期望程序所呆的地方,我們必須明確地告訴 shell 它的位置,通過 在命令之前加上 ./ 字符,來表明程序位于當前工作目錄:
~~~
[me@linuxbox diction-1.11]$ ./configure
~~~
configure 將會輸出許多信息,隨著它測試和配置整個構建過程。當結束后,輸出結果看起來像這樣:
~~~
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 是一個普通文本文件,所以我們能查看它:
~~~
[me@linuxbox 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 命令并構建我們的程序:
~~~
[me@linuxbox diction-1.11]$ make
~~~
這個 make 程序將會運行,使用 Makefile 文件的內容來指導它的行為。它會產生很多信息。
當 make 程序運行結束后,現在我們將看到所有的目標文件出現在我們的目錄中。
~~~
[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,我們開始要構建的程序。恭喜一切正常!我們剛才源碼編譯了 我們的第一個程序。但是出于好奇,讓我們再運行一次 make 程序:
~~~
[me@linuxbox diction-1.11]$ make
make: Nothing to be done for `all'.
~~~
它只是產生這樣一條奇怪的信息。怎么了?為什么它沒有重新構建程序呢?啊,這就是 make 奇妙之處了。make 只是構建 需要構建的部分,而不是簡單地重新構建所有的內容。由于所有的目標文件都存在,make 確定沒有任何事情需要做。 我們可以證明這一點,通過刪除一個目標文件,然后再次運行 make 程序,看看它做些什么。讓我們去掉一個中間目標文件:
~~~
[me@linuxbox diction-1.11]$ rm getopt.o
[me@linuxbox diction-1.11]$ make
~~~
我們看到 make 重新構建了 getopt.o 文件,并重新鏈接了 diction 和 style 程序,因為它們依賴于丟失的模塊。 這種行為也指出了 make 程序的另一個重要特征:它保持目標文件是最新的。make 堅持目標文件要新于它們的依賴文件。 這個非常有意義,做為一名程序員,經常會更新一點兒源碼,然后使用 make 來構建一個新版本的成品。make 確保 基于更新的代碼構建了需要構建的內容。如果我們使用 touch 程序,來“更新”其中一個源碼文件,我們看到發生了這樣的事情:
~~~
[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
~~~
運行 make 之后,我們看到目標文件已經更新于它的依賴文件:
~~~
[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 內核(一個經歷著不斷修改和改進的程序)包含了幾百萬行代碼。
### 安裝程序
打包良好的源碼經常包括一個特別的 make 目標文件,叫做 install。這個目標文件將在系統目錄中安裝最終的產品,以供使用。 通常,這個目錄是 /usr/local/bin,為在本地所構建軟件的傳統安裝位置。然而,通常普通用戶不能寫入該目錄,所以我們必須變成超級用戶, 來執行安裝操作:
~~~
[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!
~~~
## 總結
在這一章中,我們已經知道了三個簡單命令:
~~~
./configure
make
make install
~~~
可以用來構建許多源碼包。我們也知道了在程序維護過程中,make 程序起到了舉足輕重的作用。make 程序可以用到 任何需要維護一個目標/依賴關系的任務中,不僅僅為了編譯源代碼。
## 拓展閱讀
* Wikipedia 上面有關于編譯器和 make 程序的好文章:
[http://en.wikipedia.org/wiki/Compiler](http://en.wikipedia.org/wiki/Compiler)
[http://en.wikipedia.org/wiki/Make_(software)](http://en.wikipedia.org/wiki/Make_(software))
* GNU Make 手冊
[http://www.gnu.org/software/make/manual/html_node/index.html](http://www.gnu.org/software/make/manual/html_node/index.html)
- 第一章:引言
- 第二章:什么是shell
- 第三章:文件系統中跳轉
- 第四章:研究操作系統
- 第五章:操作文件和目錄
- 第六章:使用命令
- 第七章:重定向
- 第八章:從shell眼中看世界
- 第九章:鍵盤高級操作技巧
- 第十章:權限
- 第十一章:進程
- 第十二章:shell環境
- 第十三章:VI簡介
- 第十四章:自定制shell提示符
- 第十五章:軟件包管理
- 第十六章:存儲媒介
- 第十七章:網絡系統
- 第十八章:查找文件
- 第十九章:歸檔和備份
- 第二十章:正則表達式
- 第二十一章:文本處理
- 第二十二章:格式化輸出
- 第二十三章:打印
- 第二十四章:編譯程序
- 第二十五章:編寫第一個shell腳本
- 第二十六章:啟動一個項目
- 第二十七章:自頂向下設計
- 第二十八章:流程控制 if分支結構
- 第二十九章:讀取鍵盤輸入
- 第三十章:流程控制 while/until 循環
- 第三十一章:疑難排解
- 第三十二章:流程控制 case分支
- 第三十三章:位置參數
- 第三十四章:流程控制 for循環
- 第三十五章:字符串和數字
- 第三十六章:數組
- 第三十七章:奇珍異寶