在討論程式碼是什麼之前,我們先來談論一下什麼是可執行檔?我們說過,在Linux 系統上面,一個檔案能不能被執行看的是有沒有可執行的那個權限 (具有x permission),不過,Linux 系統上真正認識的可執行檔其實是二進位檔案 (binary program),例如 /usr/bin/passwd, /bin/touch 這些個檔案即為二進位程式碼。
或許你會說 shell scripts 不是也可以執行嗎?其實 shell scripts 只是利用 shell (例如 bash) 這支程式的功能進行一些判斷式,而最終執行的除了 bash 提供的功能外,仍是呼叫一些已經編譯好的二進位程式來執行的呢!當然啦, bash 本身也是一支二進位程式啊!那麼我怎麼知道一個檔案是否為 binary 呢?還記得我們在第七章裡面提到的 file 這個指令的功能嗎?對啦!用他就是了!我們現在來測試一下:
# 先以系統的檔案測試看看: [root@www ~]# file /bin/bash /bin/bash: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/ Linux 2.6.9, dynamically linked (uses shared libs), for GNU/Linux 2.6.9, stripped # 如果是系統提供的 /etc/init.d/syslog 呢? [root@www ~]# file /etc/init.d/syslog /etc/init.d/syslog: Bourne-Again shell script text executable |
看到了吧!如果是 binary 而且是可以執行的時候,他就會顯示執行檔類別 (ELF 32-bit LSB executable),同時會說明是否使用動態函式庫 (shared libs),而如果是一般的 script ,那他就會顯示出 text executables 之類的字樣!
既然 Linux 作業系統真正認識的其實是 binary program,那麼我們是如何做出這樣的一支binary 的程式呢?首先,我們必須要寫程式,用什麼東西寫程式?就是一般的文書處理器啊!鳥哥都喜歡使用vim 來進行程式的撰寫,寫完的程式就是所謂的原始程式碼囉!這個程式碼檔案其實就是一般的純文字檔。在完成這個原始碼檔案的編寫之後,再來就是要將這個檔案『編譯』成為作業系統看的懂得binary program 囉!而要編譯自然就需要『編譯器』來動作,經過編譯器的編譯與連結之後,就會產生一支可以執行的 binary program 囉。
舉個例子來說,在 Linux 上面最標準的程式語言為 C ,所以我使用 C 的語法進行原始程式碼的書寫,寫完之後,以Linux 上標準的 C 語言編譯器 gcc 這支程式來編譯,就可以製作一支可以執行的binary program 囉。整個的流程有點像這樣:
![利用 gcc 編譯器進行程式的編譯流程示意圖 利用 gcc 編譯器進行程式的編譯流程示意圖](http://linux.vbird.org/linux_basic/0520source//source_to_binary.gif)
圖 1.1.1、利用 gcc 編譯器進行程式的編譯流程示意圖"
事實上,在編譯的過程當中還會產生所謂的目標檔 (Object file),這些檔案是以 *.o 的副檔名樣式存在的!至於 C 語言的原始碼檔案通常以 *.c 作為副檔名。此外,有的時候,我們會在程式當中『引用、呼叫』其他的外部副程式,或者是利用其他軟體提供的『函數功能』,這個時候,我們就必須要在編譯的過程當中,將該函式庫給他加進去,如此一來,編譯器就可以將所有的程式碼與函式庫作一個連結 (Link) 以產生正確的執行檔囉。
總之,我們可以這麼說:
- 開放源碼:就是程式碼,寫給人類看的程式語言,但機器並不認識,所以無法執行;
- 編譯器:將程式碼轉譯成為機器看的懂得語言,就類似翻譯者的角色;
- 可執行檔:經過編譯器變成二進位程式後,機器看的懂所以可以執行的檔案。
在我的ubantu 11.04上编译代码是
首先新建源码
vim hello.c
然后在vim里面写源码,这个与windows下写c源码无差异。
然后再编译这个源码
gcc -o hello hello.c
这个的意思是用gcc这个编译器把hello.c源码编译成hello这个可执行文件,存在当下目录
./hello
这是是在当下目录运行hello这个binary program
就OK啦。一个最简单的例子。
在前一小節的圖1.1.1示意圖中,在編譯的過程裡面有提到函式庫這東西。什麼是函式庫呢?先舉個例子來說:我們的 Linux 系統上通常已經提供一個可以進行身份驗證的模組,就是在第十四章提到的 PAM 模組。這個PAM 提供的功能可以讓很多的程式在被執行的時候,除了可以驗證使用者登入的資訊外,還可以將身份確認的資料記錄在登錄檔裡面,以方便系統管理員的追蹤!
既然有這麼好用的功能,那如果我要編寫具有身份認證功能的程式時,直接引用該 PAM 的功能就好啦,如此一來,我就不需要重新設計認證機制囉!也就是說,只要在我寫的程式碼裡面,設定去呼叫 PAM 的函式功能,我的程式就可以利用 Linux 原本就有的身份認證的程序咯!除此之外,其實我們的 Linux 核心也提供了相當多的函式庫來給硬體開發者利用喔。
函式庫又分為動態與靜態函式庫,這兩個咚咚的分別我們在後面的小節再加以說明。這裡我們以一個簡單的流程圖,來示意一支有呼叫外部函式庫的程式的執行情況。
![程式執行時引用外部動態函式庫的示意圖 程式執行時引用外部動態函式庫的示意圖](http://linux.vbird.org/linux_basic/0520source//library.gif)
圖 1.2.1、程式執行時引用外部動態函式庫的示意圖
很簡單的示意圖啊!^_^!而如果要在程式裡面加入引用的函式庫,就需要如圖 1.1.1 所示,亦即在編譯的過程當中,就需要加入函式庫的相關設定囉。事實上, Linux 的核心提供很多的核心相關函式庫與外部參數,這些核心功能在設計硬體的驅動程式的時候是相當有用的資訊,這些核心相關資訊大多放置在/usr/include, /lib, /usr/lib裡面哩!我們在本章的後續小節再來探討。反正我們可以簡單的這麼想:
- 函式庫:就類似副程式的角色,可以被呼叫來執行的一段功能函數。
事實上,使用類似 gcc 的編譯器來進行編譯的過程並不簡單,因為一套軟體並不會僅有一支程式,而是有一堆程式碼檔案。所以除了每個主程式與副程式均需要寫上一筆編譯過程的指令外,還需要寫上最終的連結程序。程式碼小的時候還好,如果是類似 WWW 伺服器軟體 (例如Apache),或者是類似核心的原始碼,動則數百 MBytes 的資料量,編譯指令會寫到瘋掉~這個時候,我們就可以使用make 這個指令的相關功能來進行編譯過程的指令簡化了!
當執行 make 時,make 會在當時的目錄下搜尋 Makefile (or makefile) 這個文字檔,而 Makefile 裡面則記錄了原始碼如何編譯的詳細資訊!make 會自動的判別原始碼是否經過變動了,而自動更新執行檔,是軟體工程師相當好用的一個輔助工具呢!
咦!make 是一支程式,會去找 Makefile ,那 Makefile 怎麼寫?通常軟體開發商都會寫一支偵測程式來偵測使用者的作業環境,以及該作業環境是否有軟體開發商所需要的其他功能,該偵測程式偵測完畢後,就會主動的建立這個Makefile 的規則檔案啦!通常這支偵測程式的檔名為 configure 或者是 config 。
咦!那為什麼要偵測作業環境呢?在第一章當中,不是曾經提過其實每個 Linux distribution 都使用同樣的核心嗎?但妳得要注意,不同版本的核心所使用的系統呼叫可能不相同,而且每個軟體所需要的相依的函式庫也不相同,同時,軟體開發商不會僅針對 Linux 開發,而是會針對整個 Unix-Like 做開發啊!所以他也必須要偵測該作業系統平台有沒有提供合適的編譯器才行!所以當然要偵測環境啊!一般來說,偵測程式會偵測的資料大約有底下這些:
- 是否有適合的編譯器可以編譯本軟體的程式碼;
- 是否已經存在本軟體所需要的函式庫,或其他需要的相依軟體;
- 作業系統平台是否適合本軟體,包括 Linux 的核心版本;
- 核心的表頭定義檔 (header include) 是否存在 (驅動程式必須要的偵測)。
至於 make 與 configure 運作流程的相關性,我們可以使用底下的圖示來示意一下啊!下圖中,妳要進行的任務其實只有兩個,一個是執行 configure 來建立 Makefile ,這個步驟一定要成功!成功之後再以 make 來呼叫所需要的資料來編譯即可!非常簡單!
![透過 configure 與 make 進行編譯示意圖 透過 configure 與 make 進行編譯示意圖](http://linux.vbird.org/linux_basic/0520source//make_configure.gif)
圖 1.3.1、透過 configure 與 make 進行編譯示意圖
由於不同的 Linux distribution 的函式庫檔案所放置的路徑,或者是函式庫的檔名訂定,或者是預設安裝的編譯器,以及核心的版本都不相同,因此理論上,妳無法在 CentOS 5.x 上面編譯出 binary program 後,還將他拿到 SuSE 上面執行,這個動作通常是不可能成功的!因為呼叫的目標函式庫位置可能不同 (參考圖1.2.1) ,核心版本更不可能相同!所以能夠執行的情況是微乎其微!所以同一套軟體要在不同的平台上面執行時,必須要重複編譯!所以才需要原始碼嘛!瞭解乎!
詳細的 make 用法與 Makefile 規則,在後續的小節裡面再探討囉!
經過上面的介紹之後,你應該比較清楚的知道原始碼、編譯器、函式庫與執行檔之間的相關性了。不過,詳細的流程可能還是不很清楚,所以,在這裡我們以一個簡單的程式範例來說明整個編譯的過程喔!趕緊進入Linux 系統,實地的操作一下底下的範例呢!
![小標題的圖示](http://linux.vbird.org/images/penguin-s.gif)
我們以 Linux 上面最常見的 C 語言來撰寫第一支程式!第一支程式最常作的就是.....在螢幕上面印出『Hello World!』的字樣~當然,這裡我們是以簡單的 C 語言來撰寫,如果你對於 C 有興趣的話,那麼請自行購買相關的書籍喔!^_^ 好了,不囉唆,立刻編輯第一支程式吧!
- 編輯程式碼,亦即原始碼
[root@www ~]# vim hello.c <==用 C 語言寫的程式副檔名建議用 .c #include <stdio.h> int main(void) { printf("Hello World\n"); } |
- 開始編譯與測試執行
[root@www ~]# gcc hello.c [root@www ~]# ll hello.c a.out -rwxr-xr-x 1 root root 4725 Jun 5 02:41 a.out <==此時會產生這個檔名 -rw-r--r-- 1 root root 72 Jun 5 02:40 hello.c [root@www ~]# ./a.out Hello World <==呵呵!成果出現了! |
在預設的狀態下,如果我們直接以 gcc 編譯原始碼,並且沒有加上任何參數,則執行檔的檔名會被自動設定為 a.out 這個檔案名稱!所以妳就能夠直接執行 ./a.out 這個執行檔啦!上面的例子很簡單吧!那個 hello.c 就是原始碼,而gcc 就是編譯器,至於 a.out 就是編譯成功的可執行 binary program 囉!咦!那如果我想要產生目標檔 (object file) 來進行其他的動作,而且執行檔的檔名也不要用預設的a.out ,那該如何是好?其實妳可以將上面的第 2 個步驟改成這樣:
[root@www ~]# gcc -c hello.c [root@www ~]# ll hello* -rw-r--r-- 1 root root 72 Jun 5 02:40 hello.c -rw-r--r-- 1 root root 868 Jun 5 02:44 hello.o <==就是被產生的目標檔 [root@www ~]# gcc -o hello hello.o [root@www ~]# ll hello* -rwxr-xr-x 1 root root 4725 Jun 5 02:47 hello <==這就是可執行檔! -o 的結果 -rw-r--r-- 1 root root 72 Jun 5 02:40 hello.c -rw-r--r-- 1 root root 868 Jun 5 02:44 hello.o [root@www ~]# ./hello Hello World
這個步驟主要是利用 hello.o 這個目標檔製作出一個名為 hello 的執行檔,詳細的 gcc 語法我們會在後續章節中繼續介紹!透過這個動作後,我們可以得到 hello 及 hello.o 兩個檔案,真正可以執行的是 hello 這個 binary program 喔!或許你會覺得,咦!只要一個動作作出 a.out 就好了,幹嘛還要先製作目標檔再做成執行檔呢?呵呵!透過下個範例,你就可以知道為什麼啦!
如果我們在一個主程式裡面又呼叫了另一個副程式呢?這是很常見的一個程式寫法,因為可以簡化整個程式的易讀性!在底下的例子當中,我們以thanks.c 這個主程式去呼叫 thanks_2.c 這個副程式,寫法很簡單:
- 撰寫所需要的主、副程式
# 1. 編輯主程式: [root@www ~]# vim thanks.c #include <stdio.h> int main(void) { printf("Hello World\n"); thanks_2(); } # 上面的 thanks_2(); 那一行就是呼叫副程式啦! [root@www ~]# vim thanks_2.c #include <stdio.h> void thanks_2(void) { printf("Thank you!\n"); }
- 進行程式的編譯與連結 (Link)
# 2. 開始將原始碼編譯成為可執行的 binary file : [root@www ~]# gcc -c thanks.c thanks_2.c [root@www ~]# ll thanks* -rw-r--r-- 1 root root 76 Jun 5 16:13 thanks_2.c -rw-r--r-- 1 root root 856 Jun 5 16:13 thanks_2.o <==編譯產生的! -rw-r--r-- 1 root root 92 Jun 5 16:11 thanks.c -rw-r--r-- 1 root root 908 Jun 5 16:13 thanks.o <==編譯產生的! [root@www ~]# gcc -o thanks thanks.o thanks_2.o [root@www ~]# ll thanks* -rwxr-xr-x 1 root root 4870 Jun 5 16:17 thanks <==最終結果會產生這玩意兒 # 3. 執行一下這個檔案: [root@www ~]# ./thanks Hello World Thank you! |
知道為什麼要製作出目標檔了嗎?由於我們的原始碼檔案有時並非僅只有一個檔案,所以我們無法直接進行編譯。這個時候就需要先產生目標檔,然後再以連結製作成為 binary 可執行檔。另外,如果有一天,你更新了 thanks_2.c 這個檔案的內容,則你只要重新編譯 thanks_2.c 來產生新的 thanks_2.o,然後再以連結製作出新的 binary 可執行檔即可!而不必重新編譯其他沒有更動過的原始碼檔案。這對於軟體開發者來說,是一個很重要的功能,因為有時候要將偌大的原始碼全部編譯完成,會花很長的一段時間呢!
此外,如果你想要讓程式在執行的時候具有比較好的效能,或者是其他的除錯功能時,可以在編譯的過程裡面加入適當的參數,例如底下的例子:
[root@www ~]# gcc -O -c thanks.c thanks_2.c <== -O 為產生最佳化的參數 [root@www ~]# gcc -Wall -c thanks.c thanks_2.c thanks.c: In function 'main': thanks.c:5: warning: implicit declaration of function 'thanks_2' thanks.c:6: warning: control reaches end of non-void function # -Wall 為產生更詳細的編譯過程資訊。上面的訊息為警告訊息 (warning) # 所以不用理會也沒有關係! |
至於更多的 gcc 額外參數功能,就得要 man gcc 囉~呵呵!可多的跟天書一樣~
呼叫外部函式庫:加入連結的函式庫
剛剛我們都僅只是在螢幕上面印出一些字眼而已,如果說要計算數學公式呢?例如我們想要計算出三角函數裡面的sin (90度角)。要注意的是,大多數的程式語言都是使用徑度而不是一般我們在計算的『角度』,180 度角約等於 3.14 徑度!嗯!那我們就來寫一下這個程式吧!
[root@www ~]# vim sin.c #include <stdio.h> int main(void) { float value; value = sin ( 3.14 / 2 ); printf("%f\n",value); } |
那要如何編譯這支程式呢?我們先直接編譯看看:
[root@www ~]# gcc sin.c sin.c: In function 'main': sin.c:5: warning: incompatible implicit declaration of built-in function 'sin' /tmp/ccsfvijY.o: In function `main': sin.c:(.text+0x1b): undefined reference to `sin' collect2: ld returned 1 exit status # 注意看到上面最後一行,會有個錯誤訊息,代表沒有成功! |
特別注意上面的錯誤訊息,唉啊!怎麼沒有編譯成功?它說的是『undefinedreference to sin』,說的是『沒有 sin 的相關定義參考值!』,為什麼會這樣呢?這是因為 C 語言裡面的 sin 函示是寫在 libm.so 這個函式庫中,而我們並沒有在原始碼裡面將這個函式庫功能加進去,所以當然就需要在編譯與連結的時候將這個函式庫給他連結進執行檔裡面啊!我們可以這樣做:
- 編譯時加入額外函式庫連結的方式:
[root@www ~]# gcc sin.c -lm -L/lib -L/usr/lib <==重點在 -lm [root@www ~]# ./a.out <==嘗試執行新檔案! 1.000000 |
特別注意,使用 gcc 編譯時所加入的那個 -lm 是有意義的,他可以拆開成兩部份來看:
- -l :是『加入某個函式庫(library)』的意思,
- m :則是 libm.so 這個函式庫,其中, lib 與副檔名(.a 或 .so)不需要寫
所以 -lm 表示使用 libm.so (或 libm.a) 這個函式庫的意思~至於那個 -L 後面接的路徑呢?這表示:『我要的函式庫 libm.so 請到 /lib 或 /usr/lib 裡面搜尋!』
上面的說明很清楚了吧!不過,要注意的是,由於 Linux 預設是將函式庫放置在 /lib 與 /usr/lib 當中,所以你沒有寫 -L/lib 與 -L/usr/lib 也沒有關係的!不過,萬一哪天你使用的函式庫並非放置在這兩個目錄下,那麼-L/path 就很重要了!否則會找不到函式庫喔!
除了連結的函式庫之外,你或許已經發現一個奇怪的地方,那就是在我們的sin.c 當中第一行『 #include<stdio.h>』,這行說的是要將一些定義資料由 stdio.h 這個檔案讀入,這包括 printf 的相關設定。這個檔案其實是放置在 /usr/include/stdio.h的!那麼萬一這個檔案並非放置在這裡呢?那麼我們就可以使用底下的方式來定義出要讀取的include 檔案放置的目錄:
[root@www ~]# gcc sin.c -lm -I/usr/include
|
-I/path 後面接的路徑( Path )就是設定要去搜尋相關的include 檔案的目錄啦!不過,同樣的,預設值是放置在 /usr/include 底下,除非你的include 檔案放置在其他路徑,否則也可以略過這個項目!
透過上面的幾個小範例,你應該對於 gcc 以及原始碼有一定程度的認識了,再接下來,我們來稍微整理一下gcc 的簡易使用方法吧!
gcc 的簡易用法 (編譯、參數與鏈結)
前面說過, gcc 為 Linux 上面最標準的編譯器,這個 gcc 是由 GNU 計畫所維護的,有興趣的朋友請自行前往參考。既然 gcc 對於 Linux 上的 Open source是這麼樣的重要,所以底下我們就列舉幾個 gcc 常見的參數,如此一來大家應該更容易瞭解原始碼的各項功能吧!
# 僅將原始碼編譯成為目標檔,並不製作連結等功能: [root@www ~]# gcc -c hello.c # 會自動的產生 hello.o 這個檔案,但是並不會產生 binary 執行檔。 # 在編譯的時候,依據作業環境給予最佳化執行速度 [root@www ~]# gcc -O hello.c -c # 會自動的產生 hello.o 這個檔案,並且進行最佳化喔! # 在進行 binary file 製作時,將連結的函式庫與相關的路徑填入 [root@www ~]# gcc sin.c -lm -L/usr/lib -I/usr/include # 這個指令較常下達在最終連結成 binary file 的時候, # -lm 指的是 libm.so 或 libm.a 這個函式庫檔案; # -L 後面接的路徑是剛剛上面那個函式庫的搜尋目錄; # -I 後面接的是原始碼內的 include 檔案之所在目錄。 # 將編譯的結果輸出成某個特定檔名 [root@www ~]# gcc -o hello hello.c # -o 後面接的是要輸出的 binary file 檔名 # 在編譯的時候,輸出較多的訊息說明 [root@www ~]# gcc -o hello hello.c -Wall # 加入 -Wall 之後,程式的編譯會變的較為嚴謹一點, # 所以警告訊息也會顯示出來! |
比較重要的大概就是這一些。另外,我們通常稱-Wall 或者 -O 這些非必要的參數為旗標 (FLAGS),因為我們使用的是 C 程式語言,所以有時候也會簡稱這些旗標為CFLAGS ,這些變數偶爾會被使用的喔!尤其是在後頭會介紹的 make 相關的用法時,更是重要的很吶! ^_^
用 make 進行巨集編譯
在本章一開始我們提到過 make 的功能是可以簡化編譯過程裡面所下達的指令,同時還具有很多很方便的功能!那麼底下咱們就來試看看使用make 簡化下達編譯指令的流程吧!
![小標題的圖示](http://linux.vbird.org/images/penguin-s.gif)
先來想像一個案例,假設我的執行檔裡面包含了四個原始碼檔案,分別是 main.c haha.c sin_value.c cos_value.c 這四個檔案,這四個檔案的目的是:
- main.c :主要的目的是讓使用者輸入角度資料與呼叫其他三支副程式;
- haha.c :輸出一堆有的沒有的訊息而已;
- sin_value.c :計算使用者輸入的角度(360) sin 數值;
- cos_value.c :計算使用者輸入的角度(360) cos 數值。
這四個檔案你可以到 http://linux.vbird.org/linux_basic/0520source/main.tgz來下載。由於這四個檔案裡面包含了相關性,並且還用到數學函式在裡面,所以如果你想要讓這個程式可以跑,那麼就需要這樣編譯:
# 1. 先進行目標檔的編譯,最終會有四個 *.o 的檔名出現: [root@www ~]# gcc -c main.c [root@www ~]# gcc -c haha.c [root@www ~]# gcc -c sin_value.c [root@www ~]# gcc -c cos_value.c # 2. 再進行連結成為執行檔,並加入 libm 的數學函式,以產生 main 執行檔: [root@www ~]# gcc -o main main.o haha.o sin_value.o cos_value.o \ > -lm -L/usr/lib -L/lib # 3. 本程式的執行結果,必須輸入姓名、360 度角的角度值來計算: [root@www ~]# ./main Please input your name: VBird <==這裡先輸入名字 Please enter the degree angle (ex> 90): 30 <==輸入以 360 度角為主的角度 Hi, Dear VBird, nice to meet you. <==這三行為輸出的結果喔! The Sin is: 0.50 The Cos is: 0.87 |
編譯的過程需要進行好多動作啊!而且如果要重新編譯,則上述的流程得要重新來一遍,光是找出這些指令就夠煩人的了!如果可以的話,能不能一個步驟就給他完成上面所有的動作呢?那就利用 make 這個工具吧!先試看看在這個目錄下建立一個名為 makefile 的檔案,內容如下:
# 1. 先編輯 makefile 這個規則檔,內容只要作出 main 這個執行檔 [root@www ~]# vim makefile main: main.o haha.o sin_value.o cos_value.o gcc -o main main.o haha.o sin_value.o cos_value.o -lm # 注意:第二行的 gcc 之前是 <tab> 按鍵產生的空格喔! # 2. 嘗試使用 makefile 制訂的規則進行編譯的行為: [root@www ~]# rm -f main *.o <==先將之前的目標檔去除 [root@www ~]# make cc -c -o main.o main.c cc -c -o haha.o haha.c cc -c -o sin_value.o sin_value.c cc -c -o cos_value.o cos_value.c gcc -o main main.o haha.o sin_value.o cos_value.o -lm # 此時 make 會去讀取 makefile 的內容,並根據內容直接去給他編譯相關的檔案囉! # 3. 在不刪除任何檔案的情況下,重新執行一次編譯的動作: [root@www ~]# make make: `main' is up to date. # 看到了吧!是否很方便呢!只會進行更新 (update) 的動作而已。 |
或許你會說:『如果我建立一個 shell script 來將上面的所有動作都集結在一起,不是具有同樣的效果嗎?』呵呵!效果當然不一樣,以上面的測試為例,我們僅寫出 main 需要的目標檔,結果 make 會主動的去判斷每個目標檔相關的原始碼檔案,並直接予以編譯,最後再直接進行連結的動作!真的是很方便啊!此外,如果我們更動過某些原始碼檔案,則 make 也可以主動的判斷哪一個原始碼與相關的目標檔檔案有更新過,並僅更新該檔案,如此一來,將可大大的節省很多編譯的時間呢!要知道,某些程式在進行編譯的行為時,會消耗很多的CPU 資源呢!所以說, make 有這些好處:
- 簡化編譯時所需要下達的指令;
- 若在編譯完成之後,修改了某個原始碼檔案,則 make 僅會針對被修改了的檔案進行編譯,其他的object file 不會被更動;
- 最後可以依照相依性來更新 (update) 執行檔。
既然 make 有這麼多的優點,那麼我們當然就得好好的瞭解一下 make 這個令人關心的傢伙啦!而 make 裡面最需要注意的大概就是那個規則檔案,也就是 makefile 這個檔案的語法啦!所以底下我們就針對 makefile 的語法來加以介紹囉。
makefile 的基本語法與變數
make 的語法可是相當的多而複雜的,有興趣的話可以到 GNU (註1)去查閱相關的說明,鳥哥這裡僅列出一些基本的規則,重點在於讓讀者們未來在接觸原始碼時,不會太緊張啊!好了,基本的 makefile 規則是這樣的:
標的(target): 目標檔1 目標檔2
<tab> gcc -o 欲建立的執行檔 目標檔1 目標檔2
|
那個標的 (target) 就是我們想要建立的資訊,而目標檔就是具有相關性的 object files ,那建立執行檔的語法就是以 <tab> 按鍵開頭的那一行!特別給他留意喔,『命令列必須要以 tab 按鍵作為開頭』才行!他的規則基本上是這樣的:
- 在 makefile 當中的 # 代表註解;
- <tab> 需要在命令行 (例如 gcc 這個編譯器指令) 的第一個字元;
- 標的 (target) 與相依檔案(就是目標檔)之間需以『:』隔開。
同樣的,我們以剛剛上一個小節的範例進一步說明,如果我想要有兩個以上的執行動作時,例如下達一個指令就直接清除掉所有的目標檔與執行檔,該如何製作呢?
# 1. 先編輯 makefile 來建立新的規則,此規則的標的名稱為 clean : [root@www ~]# vi makefile main: main.o haha.o sin_value.o cos_value.o gcc -o main main.o haha.o sin_value.o cos_value.o -lm clean: rm -f main main.o haha.o sin_value.o cos_value.o # 2. 以新的標的 (clean) 測試看看執行 make 的結果: [root@www ~]# make clean <==就是這裡!透過 make 以 clean 為標的 rm -rf main main.o haha.o sin_value.o cos_value.o |
如此一來,我們的 makefile 裡面就具有至少兩個標的,分別是 main 與 clean ,如果我們想要建立 main 的話,輸入『make main』,如果想要清除有的沒的,輸入『makeclean』即可啊!而如果想要先清除目標檔再編譯 main 這個程式的話,就可以這樣輸入:『make clean main』,如下所示:
[root@www ~]# make clean main
rm -rf main main.o haha.o sin_value.o cos_value.o
cc -c -o main.o main.c
cc -c -o haha.o haha.c
cc -c -o sin_value.o sin_value.c
cc -c -o cos_value.o cos_value.c
gcc -o main main.o haha.o sin_value.o cos_value.o -lm
|
這樣就很清楚了吧!但是,你是否會覺得,咦! makefile 裡面怎麼重複的資料這麼多啊!沒錯!所以我們可以再藉由 shellscript 那時學到的『變數』來更簡化 makefile 喔:
[root@www ~]# vi makefile LIBS = -lm OBJS = main.o haha.o sin_value.o cos_value.o main: ${OBJS} gcc -o main ${OBJS} ${LIBS} clean: rm -f main ${OBJS} |
與 bash shell script 的語法有點不太相同,變數的基本語法為:
- 變數與變數內容以『=』隔開,同時兩邊可以具有空格;
- 變數左邊不可以有 <tab> ,例如上面範例的第一行 LIBS 左邊不可以是 <tab>;
- 變數與變數內容在『=』兩邊不能具有『:』;
- 在習慣上,變數最好是以『大寫字母』為主;
- 運用變數時,以 ${變數} 或 $(變數) 使用;
- 在該 shell 的環境變數是可以被套用的,例如提到的 CFLAGS 這個變數!
- 在指令列模式也可以給予變數。
由於 gcc 在進行編譯的行為時,會主動的去讀取 CFLAGS這個環境變數,所以,你可以直接在 shell 定義出這個環境變數,也可以在makefile 檔案裡面去定義,更可以在指令列當中給予這個咚咚呢!例如:
[root@www ~]# CFLAGS="-Wall" make clean main # 這個動作在上 make 進行編譯時,會去取用 CFLAGS 的變數內容! |
也可以這樣:
[root@www ~]# vi makefile LIBS = -lm OBJS = main.o haha.o sin_value.o cos_value.o CFLAGS = -Wall main: ${OBJS} gcc -o main ${OBJS} ${LIBS} clean: rm -f main ${OBJS} |
咦!我可以利用指令列進行環境變數的輸入,也可以在檔案內直接指定環境變數,那萬一這個CFLAGS 的內容在指令列與 makefile 裡面並不相同時,以那個方式輸入的為主?呵呵!問了個好問題啊!環境變數取用的規則是這樣的:
- make 指令列後面加上的環境變數為優先;
- makefile 裡面指定的環境變數第二;
- shell 原本具有的環境變數第三。
此外,還有一些特殊的變數需要瞭解的喔:
- $@:代表目前的標的(target)
所以我也可以將 makefile 改成:
[root@www ~]# vi makefile LIBS = -lm OBJS = main.o haha.o sin_value.o cos_value.o CFLAGS = -Wall main: ${OBJS} gcc -o $@ ${OBJS} ${LIBS} <==那個 $@ 就是 main ! clean: rm -f main ${OBJS} |
這樣是否稍微瞭解了 makefile (也可能是 Makefile) 的基本語法?這對於你未來自行修改原始碼的編譯規則時,是很有幫助的喔!^_^!