共享程序庫通過版本號來完成對應(yīng)用程序所使用的程序庫的升級,同時保留了對原有應(yīng)用程序的兼容。本文將討論此方法的實(shí)際內(nèi)幕,以及在常規(guī) Linux? 系統(tǒng)上的 /usr/lib 中有很多符號鏈接的原因。
共享程序庫是現(xiàn)代 UNIX? 系統(tǒng)中有效利用空間和資源的基礎(chǔ)。SUSE 系統(tǒng)中的 C 程序庫大約有 1.3 MB。為 /usr/bin 中每一個程序(我有 2,569 個)制作副本將占去幾個 G 的空間。
當(dāng)然這個數(shù)字有一些夸張 —— 靜態(tài)鏈接程序只合并它們使用的那部分程序庫。盡管如此, printf() 的所有副本所占用的空間數(shù)量也會讓系統(tǒng)顯得非常臃腫。
共享程序庫不僅可以節(jié)省磁盤空間,而且還可以節(jié)省內(nèi)存。內(nèi)核可以在內(nèi)存中保持某個共享程序庫的一個惟一副本,并在多個應(yīng)用程序間共享這個副本。所以,我們不但可以在磁盤上只有 printf() 的一個副本,而且在內(nèi)存中也只需要一個副本。這對性能有很大的影響。
在本文中,我們將討論共享程序庫所使用的底層技術(shù),以及在共享程序庫版本號幫助下預(yù)防兼容性難題的方法,過去,本機(jī)共享程序庫實(shí)現(xiàn)也曾遇到過這些難題。首先來看一下共享程序庫的工作原理。
共享程序庫的工作原理
這個概念理解起來非常簡單。擁有一個程序庫;然后共享這個程序庫。但是,當(dāng)您的程序嘗試調(diào)用 printf() 時,也就是說實(shí)際操作的時候,具體發(fā)生的事情卻稍微有點(diǎn)復(fù)雜。
這個過程在靜態(tài)鏈接系統(tǒng)中比在動態(tài)鏈接系統(tǒng)中更簡單。在靜態(tài)鏈接系統(tǒng)中,生成的代碼會持有對某個函數(shù)的引用。鏈接器使用加載該函數(shù)的真實(shí)地址去替換這個引用,以便生成的二進(jìn)制代碼在適當(dāng)?shù)奈恢脮姓_的地址。然后,在運(yùn)行代碼時,只需要跳轉(zhuǎn)到相應(yīng)的地址即可。對管理員來說,這是一項(xiàng)簡單的任務(wù),因?yàn)樗试S您對只在程序中的某個位置上實(shí)際引用的那些對象進(jìn)行鏈接。
但是大部分共享程序庫都是動態(tài)鏈接的。這具有一些更深層次的意義。其中一方面是,您不能事先預(yù)計(jì)某個函數(shù)在調(diào)用時的確切地址!(以及靜態(tài)鏈接的共享程序庫模式,比如 BSD/OS 中的,但是它們不在本文討論范圍之內(nèi)。)
動態(tài)鏈接器可以為每個被鏈接的函數(shù)做相當(dāng)多的工作,所以大部分鏈接器都是不積極的。只有在函數(shù)被調(diào)用時,它們才實(shí)際做一些工作。C 程序庫中有一千多個外部可見的符號,有大約三千多個本地符號,因此這種方法可以節(jié)省非常多的時間。
實(shí)現(xiàn)此奇妙功能的是一個稱為 過程鏈接表(Procedure Linkage Table)(PLT)的數(shù)據(jù)塊,它是程序中的一個表,列出了程序所調(diào)用的每一個函數(shù)。當(dāng)程序開始運(yùn)行時,PLT 包含每個函數(shù)的代碼,以便查詢運(yùn)行期鏈接器,從而獲得已加載某個函數(shù)的地址。然后它會在表中填入這個條目并跳轉(zhuǎn)到那個已加載函數(shù)。當(dāng)每個函數(shù)被調(diào)用時,它的 PLT 中的條目就會被簡化為一個到那個已加載函數(shù)的直接跳轉(zhuǎn)。
不過,重要的是,要注意到還有一個間接的額外層次 —— 可以通過跳轉(zhuǎn)到某個表來解析每個函數(shù)調(diào)用。
兼容性不僅是為了關(guān)聯(lián)
這意味著您最終要鏈接的程序庫最好與調(diào)用它的代碼相兼容。使用靜態(tài)鏈接的可執(zhí)行文件,可以在某種程度上保證不會發(fā)生任何改變。如果使用動態(tài)鏈接,就得不到這樣的保證。
當(dāng)出現(xiàn)新版本的程序庫時會怎樣?特別是新版本改變了某個給定函數(shù)的調(diào)用次序時,又會怎樣?
版本號可以解決這個問題 —— 共享的程序庫將擁有一個版本號。當(dāng)一個程序鏈接到某個程序庫時,程序中會存儲一個它計(jì)劃支持的版本號。如果更改程序庫,那么版本號就會不匹配,程序也就不會被鏈接到較新版本的程序庫。
不過,動態(tài)鏈接的可能優(yōu)勢之一在于修正缺陷。如果可以修正程序庫中的缺陷,而且不必重新編譯上千個程序,就可以利用這一修正功能,這將是非常令人愉快的。有時,需要鏈接到某個較新的版本。
不幸的是,這會導(dǎo)致在某些情況下,您希望鏈接到較新的版本,而在另外一些情況下,您寧愿堅(jiān)持使用較老的版本。不過,有一個解決方案 —— 使用兩類版本號:
- 主版本號表明程序庫版本之間的潛在不兼容性。
- 次要版本號表明只是修正了缺陷。
這樣,在大部分情形下,加載具有相同主版本號和更高次要版本號的程序庫是安全的;而加載主版本號更高的程序是不安全的行為。
為了讓用戶(和程序員)不必追蹤程序庫版本號和更新,系統(tǒng)提供了大量的符號鏈接。通常,其模式是:
libexample.so
將是一個指向
libexample.so.N
的鏈接,其中 N 是在系統(tǒng)中可以找到的最高的 主 版本號。
對受支持的每一個主版本號而言,
libexample.so.N
將是一個指向
libexample.so.N.M
的鏈接,其中 M 是最高的 次要 版本號。
這樣,如果為鏈接器指定了 -lexample,那么它會去尋找 libexample.so,這是一個符號鏈接,指向某個指向最新版本的符號鏈接。另一方面,當(dāng)加載某個現(xiàn)有程序時,它將嘗試去加載 libexample.so.N,其中 N 是它先前鏈接的版本。各得其所!
?
為了進(jìn)行調(diào)試,首先必須知道如何編譯
為了調(diào)試使用共享程序庫的問題,對它們?nèi)绾尉幾g有更多一些了解會對您有所幫助。
在傳統(tǒng)的靜態(tài)程序庫中,生成的代碼通常封裝在一個程序庫文件中(其名稱以 .a 結(jié)尾),然后傳遞給鏈接器。在動態(tài)程序庫中,程序庫文件的名稱通常以 .so 結(jié)尾。文件結(jié)構(gòu)稍有不同。
常規(guī)的靜態(tài)程序庫的格式是 ar 工具(一個非常簡單的存檔程序,類似于 tar,但是更簡單)所創(chuàng)建的那種格式。相反,共享程序庫通常以更復(fù)雜的文件格式存儲。
在現(xiàn)代 Linux 系統(tǒng)中,這一格式通常是 ELF 二進(jìn)制格式(可執(zhí)行與可鏈接格式(Executable and Linkable Format))。在 ELF 中,每個文件的組成包括:一個 ELF 頭,隨后是零或者一些段(segments),以及零或者一些區(qū)段(sections)。 段 中包含文件的運(yùn)行時執(zhí)行所需要的信息,而 區(qū)段 中包含用于鏈接和重定位的重要數(shù)據(jù)。整個文件中的每個字節(jié)每次只能由一個區(qū)段使用,不過可以存在不被任何區(qū)段所包含的孤立字節(jié)。通常,在 UNIX 可執(zhí)行文件中,一個或多個區(qū)段會封裝在一個段內(nèi)。
ELF 格式中包含用于應(yīng)用程序和程序庫的規(guī)范。但程序庫格式要復(fù)雜得多,不僅僅是對象模塊的簡單存檔。
鏈接器將所有對符號的引用進(jìn)行分類,標(biāo)識出它們是在哪個程序庫中找到的。將靜態(tài)程序庫的符號添加到最終的可執(zhí)行文件中;然后將共享程序庫的符號放入 PLT 中,最后創(chuàng)建對 FLT 的引用。在完成這些任務(wù)之后,生成的可執(zhí)行文件會擁有一個列表,該列表列出了計(jì)劃從運(yùn)行期將加載的程序庫中找出的那些符號。
在運(yùn)行期間,應(yīng)用程序?qū)⒓虞d動態(tài)鏈接器。實(shí)際上,動態(tài)鏈接器本身使用與共享程序庫相同種類的版本號。例如,在 SUSE Linux 9.1 中, /lib/ld-linux.so.2 文件是一個指向 /lib/ld-linux.so.2.3.3 的符號鏈接。另一方面,尋找 /lib/ld-linux.so.1 的程序不會嘗試使用新的版本。
然后動態(tài)鏈接器開始進(jìn)行所有有趣的工作。它會查明某個程序先前鏈接到了哪些程序庫(以及哪個版本),然后加載它們。加載程序庫的步驟包括:
- 找到程序庫(它可能在系統(tǒng)中若干個目錄中的任意一個目錄中)。
- 將程序庫映射到程序的地址空間。
- 分配程序庫可能需要的由零填充的內(nèi)存塊。
- 添加程序庫的符號表。
調(diào)試這一過程可能會比較困難。您可能會遇到多種問題。例如,如果動態(tài)鏈接器不能找到某個給定的程序庫,那么它將停止加載程序。如果它找到了所有需要的程序庫,但卻無法找到某個符號,那么它也可能會因此而停止加載操作(但是可能直到真正嘗試去引用那個符號時才會發(fā)生這種情形) —— 這是一種很少見的情況,因?yàn)橥ǔH绻淮嬖谀硞€符號,那么在初始化鏈接的時候就會被警告。
?
修改動態(tài)鏈接器的搜索路徑
當(dāng)鏈接某個程序時,在運(yùn)行期您可以指定另外的搜索路徑。在 gcc 中,其語法是 -Wl,-R/path。如果程序已經(jīng)被鏈接,那么您也可以設(shè)置環(huán)境變量 LD_LIBRARY_PATH 來改變這一行為。通常只是在應(yīng)用程序需要搜索的路徑不是系統(tǒng)級默認(rèn)路徑的一部分時才需要這樣做,對大部分 Linux 系統(tǒng)來說,這種情況很少見。理論上,Mozilla 用戶可以發(fā)布某個使用這個路徑設(shè)置所編譯的二進(jìn)制程序,但是他們更傾向于發(fā)布包裝器(wrapper)腳本,在啟動可執(zhí)行程序之前正確地設(shè)置程序庫路徑。
設(shè)置程序庫路徑可以為兩個應(yīng)用程序需要同一程序庫的不兼容版本的這種罕見情況提供一個迂回解決方案??梢允褂冒b器腳本使某一應(yīng)用程序在使用特殊版本程序庫的目錄中進(jìn)行搜索。這稱不上是一個完美的解決方案,但是在某些情況下,這是您能采用的最佳方法。
如果出于不得已的原因需要為很多程序添加某個路徑,那么也可以修改系統(tǒng)的默認(rèn)搜索路徑。通過 /etc/ld.so.conf 控制動態(tài)鏈接器,該文件包含默認(rèn)搜索路徑的列表。對 LD_LIBRARY_PATH 中指定的任何路徑的搜索都要先于 ld.so.conf 中列出的路徑,所以用戶可以覆蓋這些設(shè)置。
大部分用戶沒有理由修改系統(tǒng)默認(rèn)程序庫搜索路徑;通常環(huán)境變量更適用于修改搜索路徑,比如連接某個工具包中的程序庫,或者使用某個程序庫的較新版本的測試程序。
使用 ldd
ldd 是調(diào)試共享程序庫問題的一個實(shí)用工具。其名稱來自 list dynamic dependencies。這個程序會查看某個給定的可執(zhí)行程序或者共享程序庫,并指出它需要加載哪些共享程序庫以及要使用哪些版本。輸出類似如下:
清單 1. /bin/sh 的依賴
$ ldd /bin/sh linux-gate.so.1 => (0xffffe000) libreadline.so.4 => /lib/libreadline.so.4 (0x40036000) libhistory.so.4 => /lib/libhistory.so.4 (0x40062000) libncurses.so.5 => /lib/libncurses.so.5 (0x40069000) libdl.so.2 => /lib/libdl.so.2 (0x400af000) libc.so.6 => /lib/tls/libc.so.6 (0x400b2000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
?
看到一個“簡單的”的程序使用了這么多個程序庫,可能會有些令人驚訝?;蛟S是 libhistory 需要 libncurses。為了查明真相,我們只需要運(yùn)行另一個 ldd 命令:
清單 2. libhistory 的依賴
$ ldd /lib/libhistory.so.4 linux-gate.so.1 => (0xffffe000) libncurses.so.5 => /lib/libncurses.so.5 (0x40026000) libc.so.6 => /lib/tls/libc.so.6 (0x4006b000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x80000000)
?
在某些情況下,可能需要為應(yīng)用程序指定另外的程序庫路徑。例如,對 Mozilla 二進(jìn)制程序嘗試運(yùn)行 ldd 所得到輸出的前幾行如下所示:
清單 3. 運(yùn)行 dll 查找不在搜索路徑中的 程序庫的結(jié)果
$ ldd /opt/mozilla/lib/mozilla-binlinux-gate.so.1 => (0xffffe000)libmozjs.so => not foundlibplds4.so => not foundlibplc4.so => not foundlibnspr4.so => not foundlibpthread.so.0 => /lib/tls/libpthread.so.0 (0x40037000)
?
為什么找不到這些程序庫?因?yàn)樗鼈儾辉诔R姷某绦驇焖阉髀窂街?。?shí)際上,它們在 /opt/mozilla/lib 中,所以,解決方案之一是將這個目錄添加到 LD_LIBRARY_PATH 中。
另一個選項(xiàng)是將路徑設(shè)置為 .,并在這個目錄下運(yùn)行 ldd,盡管這樣做更危險(xiǎn) —— 將當(dāng)前目錄添加到程序庫路徑中與將它添加到可執(zhí)行程序路徑中一樣有著潛在的危險(xiǎn)。
在這種情況下,將這些程序庫所在的目錄添加到系統(tǒng)級搜索路徑中顯然不是一個好辦法。只有 Mozilla 需要這些程序庫。
鏈接 Mozilla
說起 Mozilla,如果您覺得自己從未見過超過幾行的程序庫,那么在某種程度上,Mozilla 是一個更為典型的大型應(yīng)用程序?,F(xiàn)在您可以明白為什么 Mozilla 的啟動需要那么長時間了吧!
清單 4. mozilla-bin 的依賴性
linux-gate.so.1 => (0xffffe000)libmozjs.so => ./libmozjs.so (0x40018000)libplds4.so => ./libplds4.so (0x40099000)libplc4.so => ./libplc4.so (0x4009d000)libnspr4.so => ./libnspr4.so (0x400a2000)libpthread.so.0 => /lib/tls/libpthread.so.0 (0x400f5000)libdl.so.2 => /lib/libdl.so.2 (0x40105000)libgtk-x11-2.0.so.0 => /opt/gnome/lib/libgtk-x11-2.0.so.0 (0x40108000)libgdk-x11-2.0.so.0 => /opt/gnome/lib/libgdk-x11-2.0.so.0 (0x40358000)libatk-1.0.so.0 => /opt/gnome/lib/libatk-1.0.so.0 (0x403c5000)libgdk_pixbuf-2.0.so.0 => /opt/gnome/lib/libgdk_pixbuf-2.0.so.0 (0x403df000)libpangoxft-1.0.so.0 => /opt/gnome/lib/libpangoxft-1.0.so.0 (0x403f1000)libpangox-1.0.so.0 => /opt/gnome/lib/libpangox-1.0.so.0 (0x40412000)libpango-1.0.so.0 => /opt/gnome/lib/libpango-1.0.so.0 (0x4041f000)libgobject-2.0.so.0 => /opt/gnome/lib/libgobject-2.0.so.0 (0x40451000)libgmodule-2.0.so.0 => /opt/gnome/lib/libgmodule-2.0.so.0 (0x40487000)libglib-2.0.so.0 => /opt/gnome/lib/libglib-2.0.so.0 (0x4048b000)libm.so.6 => /lib/tls/libm.so.6 (0x404f7000)libstdc++.so.5 => /usr/lib/libstdc++.so.5 (0x40519000)libgcc_s.so.1 => /lib/libgcc_s.so.1 (0x405d5000)libc.so.6 => /lib/tls/libc.so.6 (0x405dd000)/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)libX11.so.6 => /usr/X11R6/lib/libX11.so.6 (0x406f3000)libXrandr.so.2 => /usr/X11R6/lib/libXrandr.so.2 (0x407ef000)libXi.so.6 => /usr/X11R6/lib/libXi.so.6 (0x407f3000)libXext.so.6 => /usr/X11R6/lib/libXext.so.6 (0x407fb000)libXft.so.2 => /usr/X11R6/lib/libXft.so.2 (0x4080a000)libXrender.so.1 => /usr/X11R6/lib/libXrender.so.1 (0x4081e000)libfontconfig.so.1 => /usr/lib/libfontconfig.so.1 (0x40826000)libfreetype.so.6 => /usr/lib/libfreetype.so.6 (0x40850000)libexpat.so.0 => /usr/lib/libexpat.so.0 (0x408b9000)
?
深入了解共享程序庫
有興趣深入了解 Linux 中的動態(tài)鏈接的用戶有很多選擇。GNU 編譯器和鏈接器工具鏈(linker tool chain)文檔都非常好,雖然其內(nèi)容是以 info 格式存儲的,而且也沒有在標(biāo)準(zhǔn)手冊頁中提及。
ld.so 的手冊頁包含有一個非常詳盡的列表,列出了改變動態(tài)鏈接器行為的變量,以及對過去曾經(jīng)使用的不同版本的動態(tài)鏈接器的說明。
大部分 Linux 文檔都假定所有共享程序庫都是動態(tài)鏈接的,因?yàn)樵?Linux 系統(tǒng)上,它們通常是這樣的。實(shí)現(xiàn)靜態(tài)鏈接的共享程序庫需要做的工作非常多,而且大部分用戶不會因此獲得任何好處,盡管支持這個特性的系統(tǒng)的性能會有顯著改變。
如果您正在使用現(xiàn)成的預(yù)先包裝好的系統(tǒng),那么您可能不會遇到太多的共享程序庫版本 —— 系統(tǒng)可能只附帶它要鏈接的那些共享程序庫版本。另一方面,如果您做過很多次更新和源代碼構(gòu)建,那么您可能最終得到多個版本的共享程序庫,因?yàn)槔习姹疽廊粫槐A?,“以防萬一”。
像平時一樣,如果想了解更多,那么就去親自實(shí)踐吧。記住,在某個系統(tǒng)上,幾乎所有程序都會引用一些相同的共享程序庫,所以,如果破壞了系統(tǒng)的某個核心共享程序庫,那么您就得去求助系統(tǒng)恢復(fù)工具了。
評論