Linux 應用開發通常要考慮三個問題,即:1)在 Linux 應用程式開發過程中遇到過標準庫連結在不同 Linux 版本下不相容的問題; 2)在 Linux 靜態庫的製作過程中發現有別於 Windows 下靜態庫的製作方法;3)在 Linux 應用程式連結第三方庫或者其他靜態庫的時候發現連結順序的煩人問題。本文就這三個問題針對 Linux 下標準庫連結和如何巧妙構建 achrive(*.a) 展開相關介紹。
兩個要知道的基本知識
Linux 應用程式因為 Linux 版本的眾多與各自獨立性,在工程製作與使用中必須熟練掌握如下兩點才能有效地工作和理想地執行。
- Linux 下標準庫連結的三種方式(全靜態 , 半靜態 (libgcc,libstdc++), 全動態)及其各自利弊。
- Linux 下如何巧妙構建 achrive(*.a),並且如何設定連結選項來解決 gcc 比較特別的連結庫的順序問題。
三種標準庫連結方式選項及對比
為了演示三種不同的標準庫連結方式對最終應用程式產生的區別, 這裡用了一個經典的示例應用程式 HelloWorld 做演示,見 清單 1 HelloWorld。
清單 1. HelloWorld
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <stdio.h> #include <iostream> using std::cout; using std::endl; int main(int argc, char* argv[]) { printf("HelloWorld!(Printed by printf)\n"); cout<<"HelloWorld!(Printed by cout)"<<endl; return 0; } |
三種標準庫連結方式的選項及區別見 表 1
表 1. 三種標準庫連結方式的選項及區別
標準庫連線方式 | 示例連線選項 | 優點 | 缺點 |
---|---|---|---|
全靜態 | -static -pthread -lrt -ldl | 不會發生應用程式在 不同 Linux 版本下的標準庫不相容問題。 | 生成的檔案比較大, 應用程式功能受限(不能呼叫動態庫等) |
全動態 | -pthread -lrt -ldl | 生成檔案是三者中最小的 | 比較容易發生應用程式在 不同 Linux 版本下標準庫依賴不相容問題。 |
半靜態 (libgcc,libstdc++) | -static-libgcc -L. -pthread -lrt -ldl | 靈活度大,能夠針對不同的標準庫採取不同的連結策略, 從而避免不相容問題發生。 結合了全靜態與全動態兩種連結方式的優點。 |
比較難識別哪些庫容易發生不相容問題, 目前只有依靠經驗積累。 某些功能會因選擇的標準庫版本而喪失。 |
上述三種標準庫連結方式中,比較特殊的是 半靜態連結方式,主要在於其還需要在連結前增加額外的一個步驟:
ln -s g++ -print-file-name=libstdc++.a
,作用是將 libstdc++.a(libstdc++ 的靜態庫)符號連結到本地工程連結目錄。
-print-file-name 在 gcc 中的解釋如下:
-print-file-name=<lib> Display the full path to library <lib>
為了區分三種不同的標準庫連結方式對最終生成的可執行檔案的影響,本文從兩個不同的維度進行分析比較:
維度一:最終生成的可執行檔案對標準庫的依賴方式(使用 ldd 命令進行分析)
ldd 簡介:該命令用於列印出某個應用程式或者動態庫所依賴的動態庫
涉及語法:ldd [OPTION]… FILE…
其他詳細說明請參閱 man 說明。
三種標準庫連結方式最終產生的應用程式的可執行檔案對於標準庫的依賴方式具體差異見 圖 1、圖 2、圖 3所示:
圖 1. 全靜態標準庫連結方式
圖 2. 全動態標準庫連結方式
圖 3. 半靜態(libgcc,libstdc++) 標準庫連結方式
通過上述三圖,可以清楚的看到,當用 全靜態標準庫的連結方式時,所生成的可執行檔案最終不依賴任何的動態標準庫,
而 全動態標準庫的連結方式會導致最終應用程式可執行檔案依賴於所有用到的標準動態庫。
區別於上述兩種方式的 半靜態連結方式則有針對性的將 libgcc 和 libstdc++ 兩個標準庫非動態連結。
(對比 圖 2與 圖 3,可見在 圖 3中這兩個標準庫的動態依賴不見了)
從實際應用當中發現,最理想的標準庫連結方式就是半靜態連結,通常會選擇將 libgcc 與 libstdc++ 這兩個標準庫靜態連結,
從而避免應用程式在不同 Linux 版本間標準庫依賴不相容的問題發生。
維度二 : 最終生成的可執行檔案大小(使用 size 命令進行分析)
size 簡介:該命令用於顯示出可執行檔案的大小
涉及語法:size objfile…
其他詳細說明請參閱 man 說明。
三種標準庫連結方式最終產生的應用程式的可執行檔案的大小具體差異見 圖 4、圖 5、圖 6所示:
圖 4. 全靜態標準庫連結方式
圖 5. 全動態標準庫連結方式
圖 6. 半靜態(libgcc,libstdc++) 標準庫連結方式
通過上述三圖可以看出,最終可執行檔案的大小隨最終所依賴的標準動態庫的數量增加而減小。
從實際應用當中發現,最理想的是 半靜態連結方式,因為該方式能夠在避免應用程式於
不同 Linux 版本間標準庫依賴不相容的問題發生的同時,使最終生成的可執行檔案大小最小化。
示例連結選項中所涉及命令(引用 GCC 原文):
-llibrary
-l library:指定所需要的額外庫
-Ldir:指定庫搜尋路徑
-static:靜態連結所有庫
-static-libgcc:靜態連結 gcc 庫
-static-libstdc++:靜態連結 c++ 庫
關於上述命令的詳細說明,請參閱 GCC 技術手冊
Linux 下靜態庫(archive)的製作方式:
涉及命令:ar
ar 簡介:處理建立、修改、提取靜態庫的操作
涉及選項:
t – 顯示靜態庫的內容
r[ab][f][u] – 更新或增加新檔案到靜態庫中
[s] – 建立文件索引
ar -M [<mri-script] – 使用 ar 指令碼處理
其他詳細說明請參閱 man 說明。
示例情景:
假設現有如 圖 7所示兩個庫檔案
圖 7. 示例靜態庫檔案
從 圖 7中可以得知,CdtLog.a 只包含 CdtLog.o 一個物件檔案 , 而 xml.a 包含 TXmlParser.o 和 xmlparser.o 兩個物件檔案
現將 CdtLog.o 提取出來,然後通過 圖 8方式建立一個新的靜態庫 demo.a,可以看出,demo.a 包含的是 CdtLog.o 以及 xml.a,
而不是我們所預期的 CdtLog.o,TXmlParser.o 和 xmlparser.o。這正是區別於 Windows 下靜態庫的製作。
圖 8. 示例靜態庫製作方式 1
這樣的 demo.a 當被連結入某個工程時,所有在 TXmlParser.o 和 xmlparser.o 定義的符號都不會被發現,從而會導致連結錯誤,
提示無法找到對應的符號。顯然,通過圖 8 方式建立 Linux 靜態庫是不正確的。
正確的方式有兩種:
- 將所有靜態庫中包含的物件檔案提取出來然後重新打包成新的靜態庫檔案。
- 用一種更加靈活的方式建立新的靜態庫檔案:ar 指令碼。
顯然,方式 1 是比較麻煩的,因為涉及到太多的檔案處理,可能還要通過不斷建立臨時目錄用於儲存中間檔案。
推薦使用如 清單 2 createlib.sh所示的 ar 指令碼方式進行建立:
清單 2 createlib.sh
1 2 3 4 5 6 7 8 9 10 11 12 13 |
rm demo.a rm ar.mac echo CREATE demo.a > ar.mac echo SAVE >> ar.mac echo END >> ar.mac ar -M < ar.mac ar -q demo.a CdtLog.o echo OPEN demo.a > ar.mac echo ADDLIB xml.a >> ar.mac echo SAVE >> ar.mac echo END >> ar.mac ar -M < ar.mac rm ar.mac |
如果想在 Linux makefile 中使用 ar 指令碼方式進行靜態庫的建立,可以編寫如 清單 3 BUILD_LIBRARY所示的程式碼:
清單 3 BUILD_LIBRARY
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
define BUILD_LIBRARY $(if $(wildcard $@),@$(RM) $@) $(if $(wildcard ar.mac),@$(RM) ar.mac) $(if $(filter %.a, $^), @ echo CREATE $@ > ar.mac @ echo SAVE >> ar.mac @ echo echo END >> ar.mac @$(AR) -M < ar.mac ) $(if $(filter %.o,$^),@$(AR) -q $@ $(filter %.o, $^)) $(if $(filter %.a, $^), @ echo OPEN $@ > ar.mac $(foreach LIB, $(filter %.a, $^), @ echo ADDLIB $(LIB) >> ar.mac ) @ echo SAVE >> ar.mac @ echo END >> ar.mac @$(AR) -M < ar.mac @$(RM) ar.mac ) endef $(TargetDir)/$(TargetFileName):$(OBJS) $(BUILD_LIBRARY) |
通過 圖 9,我們可以看到,用這種方式產生的 demo.a 才是我們想要的結果。
圖 9. 巧妙建立的靜態庫檔案結果
Linux 靜態庫連結順序問題及解決方法:
正如 GCC 手冊中提到的那樣:
It makes a difference where in the command you write this option; the linker
searches and processes libraries and object files in the order they are specified.
Thus, ‘ foo.o -lz bar.o ’ searches library ‘ z ’ after file ‘ foo.o ’ but before
‘ bar.o ’ . If ‘ bar.o ’ refers to functions in ‘ z ’ , those functions may not be loaded.
為了解決這種庫連結順序問題,我們需要增加一些連結選項 :
$(CXX) $(LINKFLAGS) $(OBJS) -Xlinker “-(” $(LIBS) -Xlinker “-)” -o $@
通過將所有需要被連結的靜態庫放入 -Xlinker “-(” 與 -Xlinker “-)” 之間,可以是 g++ 連結過程中, 自動迴圈連結所有靜態庫,從而解決了原本的連結順序問題。
涉及連結選項:-Xlinker
-Xlinker option
Pass option as an option to the linker. You can use this to supply system-specific
linker options which GCC does not know how to recognize.
小結
本文介紹了 Linux 下三種標準庫連結的方式及各自利弊,同時還介紹了 Linux 下靜態庫的製作及使用方法,相信能夠給 大多數需要部署 Linux 應用程式和編寫 Linux Makefile 的工程師提供有用的幫助。