天天看點

C 語言程式設計 — 靜态庫、動态庫和共享庫

目錄

文章目錄

程式函數庫

靜态連結

建立靜态庫檔案

動态連結

建立共享庫檔案

共享庫檔案的名字

共享庫檔案的存儲路徑

LD_LIBRARY_PATH 環境變量

ldconfig 指令

ldd 指令

參考文檔

《C 語言程式設計 — GCC 工具鍊》

《C 語言程式設計 — 程式的編譯流程》

《C 語言程式設計 — 靜态庫、動态庫和共享庫》

《C 語言程式設計 — 程式的裝載與運作》

《計算機組成原理 — 指令系統》

《C 語言程式設計 — 結構化程式流的彙編代碼與 CPU 指令集》

程式函數庫,本質是一個包含已經編譯好代碼和資料的檔案,這些編譯好的代碼和資料通常是經過高度抽象的通用邏輯,可以供其他程式使用,避免重複造輪子。程式函數庫可以使得程式的開發工作更加子產品化,更容易重新編譯,而且更友善更新。

程式函數庫可分為 3 種類型:

靜态庫(Static Libraries)

共享庫(Shared Libraries)

動态庫(Dynamically Loaded Libraries)

在 Linux 中,靜态庫命名為 lib*.a;而動态庫和共享庫本質是一個類似的東西,隻是在 Linux 中叫作共享對象 lib*.so(Share Object),而在 Window 中叫作動态加載連結, 檔案字尾為 .dll。

在 C 語言中,不管是使用哪一種庫,程式員必須在程式中通過 include 來包含相應的頭檔案,并在預編譯階段替換 include 的内容,然後在連結階段将調用到的庫函數從各自所在的檔案庫中連結到合适的地方。

C 語言程式設計 — 靜态庫、動态庫和共享庫

從上文我們知道,連結(Link)是程式被裝載到記憶體運作之前需要完成的一個步驟。連結又分為動态連結(Dynamic Link)和靜态連結(Static Link)兩種方式。

靜态連結:是指在編譯階段直接把靜态庫加入到可執行檔案中去,這樣可執行檔案會比較大。連結器将函數的代碼從其所在地(不同的目标檔案或靜态連結庫中)拷貝到最終的可執行程式中。為建立可執行檔案,連結器必須要完成的主要任務是:符号解析(把目标檔案中符号的定義和引用聯系起來)和重定位(把符号定義和記憶體位址對應起來然後修改所有對符号的引用)。

動态連結:則是指連結階段僅僅隻加入一些描述資訊,而程式執行時再從系統中把相應動态庫加載到記憶體中去。

靜态連結方式的好處是:友善程式移植,因為可執行程式包含了所有庫函數的内容,放在任何環境當中都可以執行。如果你想把自己提供的函數給别人使用,但是又想對函數的源代碼進行保密,此時就可以給别人提供一個靜态函數庫檔案。而缺點就是:可執行檔案通常會比較大。而且每次庫檔案更新的話,都要重新編譯源檔案,很不友善。

直覺的看,一個全靜态方式生成的簡單 print 程式大小為 857K,而動态連結生成的一樣的可執行檔案隻有 8.4K,因為靜态連結的可執行檔案包含了整理 stdio 庫檔案。

C 語言程式設計 — 靜态庫、動态庫和共享庫

如上圖,對于靜态編譯的程式 1、2,因為都使用了 staticMath 庫。是以在記憶體中就有兩份相同的 staticMath.o 目标檔案,一旦程式數量過多就很可能會記憶體不足,很浪費空間。

靜态函數庫檔案使用 ar(Archiver)程式建立,下面看一個例子。

add.c

add.h

生成目标檔案

生成靜态庫檔案

使用靜态庫:

編譯

-L:指定加載庫檔案的路徑。

-l:指定加載的庫檔案。

動态連結,即:在程式運作過程中動态的調用庫檔案。好處是:占空間小、程式檔案小。缺點是:可移植性太差,如果兩台電腦運作環境不同,例如:動态庫存放的位置不一樣、沒有動态庫檔案,就很可能導緻程式運作失敗。

在基于 GNU glibc 的 Linux 系統中,gcc 編譯連結時的動态庫搜尋路徑的順序通常為:

首先從 gcc 指令的參數 -L 指定的路徑尋找;

再從環境變量 LIBRARY_PATH 指定的路徑尋址;

再從預設路徑 /usr/lib64、/usr/lib、/usr/local/lib 尋找。

在基于 GNU glibc 的 Linux 系統中,運作一個 ELF 格式的可執行檔案時,系統會啟動 Program Loader(/lib/ld-linux.so.X,X 是版本号),這個 Loader 會加載可執行檔案所需要使用的所有的共享函數庫。是以,二進制檔案執行時的動态庫搜尋路徑的順序通常為:

首先搜尋編譯目标代碼時指定的動态庫搜尋路徑;

再從環境變量 LD_LIBRARY_PATH 指定的路徑尋址;

再從配置檔案 /etc/ld.so.conf 中指定的動态庫搜尋路徑;

再從預設路徑 /usr/lib64、/usr/lib 尋找。

C 語言程式設計 — 靜态庫、動态庫和共享庫

可見,差別于靜态連結,動态連結的方式使得多個程式可以使用同一個庫檔案,這就是所謂 共享對象 的含義。

在動态連結的過程中,我們希望連結的不是存儲在磁盤上的目标檔案代碼,而是連結到了記憶體中的共享庫(Shard Libraries)。這個加載到記憶體中的共享庫會被很多程式的指令調用。在 Windows 中,這個共享庫檔案就是 .dll(Dynamic-Link Libary,動态連結庫)檔案。而在 Linux 下,這些共享檔案就是 .so(Shared Object)檔案。

注意:由于連結動态庫和靜态庫的路徑可能有重合,是以如果在路徑中有同名的靜态庫檔案和動态庫檔案,比如 libtest.a 和 libtest.so,gcc 連結時預設優先選擇動态庫,連結 libtest.so,如果要讓 gcc 選擇連結 libtest.a 則可以指定 gcc 選項 -static,該選項會強制使用靜态庫進行連結。e.g.

有了動态連結方式之後,我們得以把記憶體利用得更加的極緻,共享庫檔案是有如共享單車一般的存在。因為共享庫檔案中的函數是在程式啟動時被加載的,所有的程式在重新運作的時候都可以自動加載最新的函數庫中的内容,是以,非常易于更新。Linux 系統中的共享庫檔案還有可以實作更多的功能:

更新了函數庫但是仍然允許程式使用老版本的函數庫。

當執行某個特定程式的時候可以覆寫某個特定的庫或者庫中指定的函數。

可以在庫函數被使用的過程中修改這些函數庫。

C 語言程式設計 — 靜态庫、動态庫和共享庫

不過,要想在程式中運作時加載共享庫代碼,就要求這些共享庫代碼是 “位址無關” 的。也就是說,我們編譯出來的共享庫檔案的指令代碼,是位址無關嗎。換句話說,共享庫無論加載到那個記憶體位址,都能夠正常的運作。否則,就是位址相關代碼。幸運的是,大部分函數庫代碼都是可以做到位址無關的,因為它們都被實作為接收特定的輸入,進行确定的操作,然後在傳回結果。這些函數的代碼邏輯和輸入資料存放在記憶體什麼位置并無所謂。

每個共享函數庫都有個特殊的名字(soname)。soname 必須以 lib 作為字首,然後是函數庫的名字,然後是 .so 字尾。此外,每個共享函數庫都有一個真正的名字(real name),它是包含真正庫函數代碼的檔案。真名有一個主版本号,和一個發行版本号(可選)。主版本号和發行版本号使你可以知道庫函數的真實版本。

C 語言程式設計 — 靜态庫、動态庫和共享庫

生成共享檔案

-shared :指定生成共享庫。

-fPIC :表示編譯為位置獨立的代碼,用于編譯共享庫。目标檔案需要建立成位置無關碼,就是在可執行程式裝載它們的時候,它們可以放在可執行程式的記憶體裡的任何地方。

動态連結編譯

執行程式

ERR:libadd.so: cannot open shared object file: No such file or directory

因為執行程式找不到 libadd.so,檢視 test 程式的動态連結庫資訊:

可以看到 test 執行程式用到的 libadd.so 确實是 not found,這是因為在 /etc/ld.so.conf 檔案中設定了動态連結庫了尋找路徑:

顯然這裡是沒有 libadd.so 的存儲路徑的,是以我們需要添加一下 libadd.so 的路徑:

然後執行 ldconfig 指令生效,再次執行 test 程式:

通常的,共享函數庫檔案會被存放在一些特定的目錄裡,這樣程式才能找到并使用共享庫函數。常見的是 /usr 目錄(UNIX System Resources),該目錄作為作業系統的核心,包含了作業系統發行時自帶的各種程式,以及支援這些程式的各種共享庫檔案、頭檔案、可執行檔案等等。下屬的 /usr/local 則包含了本地系統管理者自行添加的程式的庫檔案、頭檔案和可執行程式等。

/usr/lib

/usr/lib64

/usr/local/lib

/usr/local/lib64

64 位 Linux 的預設共享庫路徑為 /usr/lib64,其次才是 /usr/lib,而 /usr/local/lib、/usr/local/lib64 則不作為預設查詢路徑。GNU 标準建議所有的函數庫檔案都放在 /usr/local/lib 目錄下,而且建議指令可執行程式都放在 /usr/local/bin 目錄下。但這也隻是一個建議,并不強制要求。

環境變量 LD_LIBRARY_PATH 用于指明共享庫的檢索路徑,當我們在調試一個新的函數庫、或者在特殊的場合使用一個非标準的函數庫的時候就可以考慮使用該變量。當我們想在這預設檢索目錄以外存放共享庫,但是又不想在 /etc/ld.so.conf 中增加記錄,那麼就可以 export LD_LIBRARY_PATH 執行一個自定義的路徑。環境變量 LD_PRELOAD 則列出了需要被優先加載的共享庫檔案,功能與 /etc/ld.so.preload 類似。這些都是由 Program Loader 實作的。

當程式啟動時搜尋所有的路徑效率顯然會很低,于是 Linux 實作了一個高速緩沖機制。ldconfig 是一個共享庫管理工具,用于在周遊共享庫檢索路徑并重新整理緩存檔案 /etc/ld.so.cache。ldconfig 通常在系統啟動時,或引入了新的共享庫時執行。

ldconfig 預設情況下讀出 /etc/ld.so.conf 相關資訊,然後适當地設定符号連結,e.g.

如果共享庫不是一個符号連接配接,而是一個實體檔案的話,ldconfig 就會提示 is not asymbolic link 的錯誤。

解決辦法就是改成符号連接配接即可:

最後,再寫一個 Cache 到 /etc/ld.so.cache 這個檔案中,這個 /etc/ld.so.cache 就可以被其他程式有效的使用了。

ldconfig 指令的可用選項說明如下:

-v:顯示正在掃描的目錄及搜尋到的動态連結庫,還有它所建立的連結的名字。

-n:僅掃描指令行指定的目錄。

-N:不重建緩存檔案。

-X:不更新檔案的連結。

-f CONF:此選項指定共享庫的配置檔案,預設為 /etc/ld.so.conf。

-C CACHE:此選項指定生成的緩存檔案,預設為 /etc/ld.so.cache。

-r ROOT:此選項改變應用程式的根目錄為 ROOT(是調用 chroot 函數實作的)。

-p:列印出目前緩存檔案所儲存的所有共享庫的名字。

-V:此選項列印出 ldconfig 的版本資訊,而後退出。

ldd 指令的作用是列印可執行檔案的共享庫依賴關系。實際上 ldd 也是通過 ld-linux.so 實作的,例如:/lib/ld-linux.so.2 --list program 就相當于 ldd program。

ldd 常用來解決程式因缺少某個庫檔案而不能運作的問題,例如上文中提到的例子。

繼續閱讀