程式扎記: [GNU Make] 規則 : 管理程式庫

標籤

2010年9月16日 星期四

[GNU Make] 規則 : 管理程式庫


前言 :
程式庫 (通常簡稱為 library 或 archive) 是一個特殊的檔案, 該檔案內含其它稱為成員的檔案. 程式庫可用來將相關的目的檔聚集成較容易操作的單元. 例如 C 的標準函式庫 libc.a 就包含 許多低階的 C 函式. 因為程式庫如此常見所以 make 對它們的建立與維護提供特別的支援, 程式庫的建立與修改可以透過 ar 程式來進行. 現在看一個例子. 我們可以把單字計數程式中可重複使用的部分擺到一個可重複使用的程式庫裡. 這個程式庫組成自兩個檔案 : counter.o 與 lexer.o. 我們可以藉由下面命令來建立此程式庫 :
[benjamin@localhost ~/src/gnumkproj]$ ar rv libcounter.a counter.o lexer.o
ar: warning: creating libcounter.a
a - counter.o
a - lexer.o

其中選項 r 代表我們想要以所指定目的擋來置換 (replace) 程式庫的成員, 而選項 v 則代表 ar 程式必須詳細的列印執行過程. rv 選項之後的第一個引數是程式庫的檔名, 接著是一連串的目的檔 (如果該程式庫不存在, 某些版本的 ar 必須指定 c 選項才會進行建立程式庫的動作, 不過 GNU ar 不必這麼做.ar 命令執行後所顯示的訊息裡, 你將會看到它以 "a" 來表示目的檔已經被加入程式庫裡. 以 r 選項來使用 ar 命令, 可以讓我們立即建立或更新一個程式庫 :
[benjamin@localhost ~/src/gnumkproj]$ ar rv libcounter.a counter.o
r - counter.o
[benjamin@localhost ~/src/gnumkproj]$ ar rv libcounter.a lexer.o
r- lexer.o

ar 命令之後所顯示的訊息裡, 你會看到 "r" 字元代表該目的檔已被置換到程式庫中.

編譯的程式庫鏈結 :
一個程式庫被連結到一個可執行檔的方法有好幾種. 最簡單的方式就是在命令列上直接指定該程式庫. 編譯器或連結器會以檔案的副檔名來判斷命令列上特定檔案的類型, 以做對應的動作 :
# cc count_words. libcounter.a /usr/lib/libfl.a -o count_words

此處 cc 將會把 libcounter.a 與 /usr/lib/libfl.a 這兩個檔案視為程式庫, 以及對它們搜尋未定義的符號. 在命令列上參用程式庫的另一個方法就是使用 -l 選項 :
[benjamin@localhost ~/src/gnumkproj]$ cc count_words.o -lcounter -lfl -o count_words2
/usr/bin/ld: cannot find -lcounter

如你所見, 使用這個 -l 選項你將可以省略程式庫支檔名的前綴以及副檔名-l 選項可以讓命令列更加緊湊且較容易閱讀, 不過它還具有極為有用的功能. 當 CC 看到 -l 選項時, 就會在系統的標準程式庫目錄搜尋相應的程式庫. 這樣程式員就不必知道程式庫的確切位置, 而且可以讓起所用的命令列更具移植性. 此外在支援共享程式庫 (在 UNIX 系統上就是以 .so 為副檔名的程式庫) 的系統上, 連結器在搜尋程式庫之前將會先搜尋共享程式庫. 這讓程式在不指明的狀況下也能得益於共享程式庫. 這是 GNU 之連結器/編譯器預定的行為.

欲變更編譯器所使用的搜尋路徑, 你可以使用 -L 選項來指定所要搜尋的目錄以及搜尋的順序. 這些使用 -L 選項的目錄都會在標準函式庫目錄之前就被搜尋, 並且會被應用在命令列中所有的 -l 選項. 事實上前面的例子將會連結失敗. 我們只要以如下方式加入當前目錄作為搜尋路徑就可以修正此錯誤 :
[benjamin@localhost ~/src/gnumkproj]$ cc count_words.o -L. -lcounter -lfl -o count_words2

程式庫作為一個程式建構過程增添了些許複雜性. make 要如何協助我們簡化此狀態? GNU make 對程式庫的建造以及連結提供了特別的支援.

建立與更新程式庫 :
在 makefile 裡, 你指定程式庫的方式跟任何其他檔案沒有兩樣, 也就是指出他的檔名. 下面就是一個用來建立程式庫的簡單規則 :
  1. libcounter.a: counter.o lexer.o  
  2.         $(AR) $(ARFLAGS) $@ $^  

此處使用了 AR 變數中的內建定義 ar 程式, 以及 ARFLAGS 變數中的標準選項 -rv. 這個程式的檔名會被自動設定到 $@, 而必要條件會被自動設定到 $^ 裡. 現在如果你以 libcounter.a作為 count_words 的必要條件, 則 make 在每次連結可執行檔之前都會更新程式庫. 然而這就會有一個問題. 程式庫的所有成員每次都會被置換掉, 即使它們並未遭到修改. 寶貴的時間就這樣浪費掉了, 不過我們可以做得更好 :
  1. libcounter.a: counter.o lexer.o  
  2.         $(AR) $(ARFLAGS) $@ $?  

如果你將 $^ 代換成 $? , 則 make 只會把時間戳記在工作目標 (的時間戳記)之後的目的檔傳遞給 ar. 在 GNU make 中你還可以使用如下標記來參用程式庫裡的成員 :
  1. libgraphics.a(bitblt.o): bitblt.o  
  2.         $(AR) $(ARFLAGS) $@ $<  

此處程式庫檔名是 libgraphics.a , 而成員的檔名是 bitblt.o. 語法 libname.a(module.o) 可用來參照程式庫裡的特定成員, 這個工作目標的必要條件就是目的檔本身, 而所要執行的命令就是將目的檔加入程式庫. 根據以上說法, 我們在 [GNU Make] 如何撰寫一個簡單的 makefile 的 makefile 就可以改寫成如下 :
* makefile_2-8 內容 :
  1. VPATH = src include  
  2. vpath %.l %.c src  
  3. vpath %.h include  
  4. CPPFLAGS = -I include  
  5.   
  6. count_words: count_words.o libcounter.a -lfl  
  7. libcounter.a: libcounter.a(lexer.o) libcounter(counter.o)  
  8.   
  9. libcounter.a(lexer.o): lexer.o  
  10.         $(AR) $(ARFLAGS) $@ $<  
  11.   
  12. libcounter.a(counter.o): counter.o  
  13.         $(AR) $(ARFLAGS) $@ $<  
  14.   
  15. count_words.o: count_words.c counter.h  
  16. counter.o: counter.c counter.h lexer.h  
  17. lexer.o: lexer.c lexer.c  
  18. lexer.c: lexer.l  
  19. libcounter.a: counter.o lexer.o  
  20.         $(AR) $(ARFLAGS) $@ @^  

對此 makefile 執行 make 將會產生如下輸出結果 :
[benjamin@localhost ~/src/gnumkproj]$ gmake -f makefile_2-8
cc -I include -c -o count_words.o src/count_words.c
In file included from src/count_words.c:2:
include/counter.h:7:7: warning: no newline at end of file
In file included from src/count_words.c:3:
include/lexer.h:7:7: warning: no newline at end of file
src/count_words.c: In function 'main':
src/count_words.c:11: warning: incompatible implicit declaration of built-in function 'exit'
cc -I include -c -o lexer.o lexer.c
ar rv libcounter.a lexer.o
ar: warning: creating libcounter.a
a - lexer.o
cc -I include -c -o counter.o src/counter.c
In file included from src/counter.c:1:
include/lexer.h:7:7: warning: no newline at end of file
In file included from src/counter.c:2:
include/counter.h:7:7: warning: no newline at end of file
src/counter.c:10:2: warning: no newline at end of file
ar rv libcounter counter.o
r - counter.o
cc count_words.o libcounter.a /usr/lib/libfl.a -o count_words

當然這個用來管理程式庫的語法也可以使用在內定規則. GNU make 隨附有一個用來更新程式庫的內建規則, 如果我們使用這個規則, 則我們的 makefile 可以簡化成下面的內容 :
* makefile_2-8-1 內容 :
  1. VPATH = src include  
  2. vpath %.l %.c src  
  3. vpath %.h include  
  4. CPPFLAGS = -I include  
  5.   
  6. count_words: count_words.o libcounter.a -lfl  
  7. libcounter.a: libcounter.a(lexer.o) libcounter(counter.o)  
  8.   
  9. count_words.o: count_words.c counter.h  
  10. counter.o: counter.c counter.h lexer.h  
  11. lexer.o: lexer.c lexer.c  
  12. lexer.c: lexer.l  
以程式庫為必要條件 : 
當程式庫作為必要條件時, 你可以使用標準的檔名語法或 -l 語法來參照他們. 使用檔名語法時 : 

  1. xpong: $(OBJECTS) /lib/X11/libX11.a /lib/X11/libXaw.a  
  2.     $(LINK) $^ -o $@  

連結器將會只讀取命令上所列出的程式庫檔案, 以及按照正常的方式來處理他們. 使用 -l 語法時, 必要條件並非真的檔案名稱 : 

  1. xpong: $(OBJECTS) -lX11.a -lXaw.a  
  2.     $(LINK) $^ -o $@  
當 -l 的語法出現在必要條件時, make 將會搜尋相應的程式庫 (而且會先搜尋共享程式庫) 以即將它們的值 (以絕對路徑形式) 代換到變數 $^ 和 $? 裡. 第二種語法的最大優點是, 它讓你得以使用 "優先搜尋共享程式庫" 的功能, 即使系統的連結器無法進行這些工作. 總之第一種語法會忽略共享程式庫, 以及使用連結列上所指定的程式庫. 供 -l 語法辨識程式庫檔名格式的樣式就放在 .LIBPATTERNS (變量“.LIBPATTERNS”就是告訴鏈接器在執行鏈接過程中對於出現“-LNAME”的文件如何展開。當然我們也可以將此變量製空,取消鏈接器對“-lNAME”格式進行展開) 變數裡, 你可以透過他來定製其它程式庫的檔名格式. 

可惜有個小問題, 如果 makefile 已經將程式庫檔案指定為工作目標, 他就不能在必要條件裡對該檔案使用 -l 選項. 舉例來說, 對下面的 makefile : 

  1. count_words: count_words.o -lcounter -lfl  
  2.     $(CC) $^ -o $@  
  3. libcounter.a: libcounter.a(lexer.o) libcounter.a(counter.o)  

執行 make , 將會出現如下的錯誤訊息 : 

No rule to make target '-lcounter', needed by 'count_words'

這是因為 make 不會把 -lcounter 展開成 libcounter.a 接著去搜尋工作目標, make 只會搜尋程式庫. 所以要在 makefile 裡進行程式庫的建造工作, 必須使用檔名的語法. 要讓複雜程式庫的連結工作沒有錯誤, 可能需要一些手段. 連結器將會依搜尋命令列上所指定的程式庫所以如果程式庫 A 包含了一個未定義的符號, 例如 open , 而且該符號定義在程式庫 B , 那你就必須在連結命令列之上於 B 之前指定 A (也就是 A 需要 B). 否則一但連結器讀進 A 並且看到未定義符號 open , 再回頭來讀取 B 就太遲了. 連結器並不會回頭來讀取前面的程式庫. 所以程式庫在命令列上的順序相當重要. 

一但工作目標的必要條件放在變數 $^ 和 $? 之後, 它們的順序就會被保存下來. 所以如果前面的例子使用 $^, 將會展開成順序跟必要條件沒有兩樣的檔案清單. 即使必要條件橫跨多項規則也是如此. 也就是說, 每項規則的必要條件會依照它們被看到順序依次加到該工作目標的必要條件清單. 

這裡順便一提的是自動變數通常會丟棄重複的部分. 舉例來說, 假如我們必須重複使用一個程式庫必要條件以滿足程式庫循環參照的需求 : 

  1. xpong: xpong.o libui.a libdynamics.a libui.a -lX1  
  2.     $(CC) $^ -o $@  

這個必要條件清單將會被處理成如下的連結命令 : 

gcc xpong.o libui.a libdynamics.a /usr/lib/X11R6/libX11.a -o xpong <少了一次 libui.a 的引用>

為了解決 $^ 的這種行為, make 另外提供了 $+ 變數. 此變數如同 $^ , 不過它會保留重複的必要條件

雙冒號規則 : 
雙冒號規則是一個含糊的功能, 他會依據必要條件的時間戳記是否在工作目標(的時間戳記)之後, 以不同的命令更新同一個工作目標. 通常, 當一個工作目標多次出現, 所有的必要條件會被銜接成一份清單, 而且只讓一支命令稿進行更新動作. 然而對雙冒號規則而言, 相同的工作目標每出現一次就會被視為一個獨立的實體, 必須進行個別處理. 這意味對同一個工作目標來說, 所有的規則必須是同一個類型, 也就是說, 它們若非全都是雙冒號規則, 就應該是全部都是單冒號規則
考慮下面的範例 : 

  1. file-list:: generate-list-script  
  2.     chmod +x $<  
  3.     generate-list-script $(files) > file-list  
  4.   
  5. file-list:: $(files)  
  6.     generate-list-script $(files) > file-list  

我們可以透過兩種方式重新產生 file-list 工作目標. 如果產生命令稿 (generate-list-script) 遭到更新, 我們就會將該命令稿設為可執行 (chmod +x ...), 接著加以執行. 如果是原代碼遭到變更, 我們只會執行該命令稿.


補充說明 :
[GNU Make] 規則 : 自動變數 與 使用VPATH/vpath 來搜尋檔案
[GNU Make] 規則 : 自動產生依存關係
This message was edited 14 times. Last update was at 06/03/2010 19:29:12

沒有留言:

張貼留言

網誌存檔