Linux 動態庫與靜態庫製作及使用詳解

簡介: Linux 應用開發通常要考慮三個問題,即:1)在 Linux 應用程序開發過程中遇到過標準庫鏈接在不同 Linux 版本下不兼容的問題; 2)在 Linux 靜態庫的製作過程中發現有別於 Windows 下靜態庫的製作方法;3)在 Linux 應用程序鏈接第三方庫或者其他靜態庫的時候發現鏈接順序的煩人問題。本文就這三個問題針對 Linux 下標準庫鏈接和如何巧妙構建 achrive(*.a) 展開相關介紹。

兩個要知道的基本知識

Linux 應用程序因爲 Linux 版本的衆多與各自獨立性,在工程製作與使用中必須熟練掌握如下兩點纔能有效地工作和理想地運行。

  1. Linux 下標準庫鏈接的三種方式(全靜態 , 半靜態 (libgcc,libstdc++), 全動態)及其各自利弊。

  2. Linux 下如何巧妙構建 achrive(*.a),並且如何設置鏈接選項來解決 gcc 比較特別的鏈接庫的順序問題。

三種標準庫鏈接方式選項及對比

爲了演示三種不同的標準庫鏈接方式對最終應用程序產生的區別, 這裏用了一個經典的示例應用程序 HelloWorld 做演示,見 清單 1 HelloWorld。 整個工程可以在文章末尾下載。


清單 1. HelloWorld
				
 #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++) 標準庫鏈接方式 


通過上述三圖,可以清楚的看到,當用 全靜態標準庫的鏈接方式時,所生成的可執行文件最終不依賴任何的動態標準庫,
而 全動態標準庫的鏈接方式會導致最終應用程序可執行文件依賴於所有用到的標準動態庫。
區別於上述兩種方式的 半靜態鏈接方式則有針對性的將 libgcc 和 libstdc++ 兩個標準庫非動態鏈接。
(對比 圖 2與 圖 3,可見在 圖 3中這兩個標準庫的動態依賴不見了)

從實際應用當中發現,最理想的標準庫鏈接方式就是半靜態鏈接,通常會選擇將 libgcc 與 libstdc++ 這兩個標準庫靜態鏈接,
從而避免應用程序在不同 Linux 版本間標準庫依賴不兼容的問題發生。

維度二 : 最終生成的可執行文件大小(使用 size 命令進行分析)

size 簡介:該命令用於顯示出可執行文件的大小 
涉及語法:size objfile...
其他詳細說明請參閱 man 說明。

三種標準庫鏈接方式最終產生的應用程序的可執行文件的大小具體差異見 圖 4圖 5圖 6所示:


圖 4. 全靜態標準庫鏈接方式
全靜態標準庫鏈接方式 

圖 5. 全動態標準庫鏈接方式
全動態標準庫鏈接方式 

圖 6. 半靜態(libgcc,libstdc++) 標準庫鏈接方式
半靜態(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
示例靜態庫製作方式 1 

這樣的 demo.a 當被鏈接入某個工程時,所有在 TXmlParser.o 和 xmlparser.o 定義的符號都不會被發現,從而會導致鏈接錯誤,
提示無法找到對應的符號。顯然,通過圖 8 方式創建 Linux 靜態庫是不正確的。

正確的方式有兩種:

  1. 將所有靜態庫中包含的對象文件提取出來然後重新打包成新的靜態庫文件。

  2. 用一種更加靈活的方式創建新的靜態庫文件:ar 腳本

顯然,方式 1 是比較麻煩的,因爲涉及到太多的文件處理,可能還要通過不斷創建臨時目錄用於保存中間文件。
推薦使用如 清單 2 createlib.sh所示的 ar 腳本方式進行創建:


清單 2 createlib.sh
				
 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
				
 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 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 的工程師提供有用的幫助。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章