Next Previous Contents

6. 連結

由於靜態與共享程式庫兩者間不相容的格式的差異性與動詞*link*過量使用於指稱*編譯完成後的事情*與*當編譯好的程式使用時所發生的事情*這兩件事上頭,使得這一章節變得複雜了許多。( and, actually, the overloading of the word `load' in a comparable but opposite sense)不過,再複雜也就是這樣了,所以閣下不必過於擔心。

為了稍微減輕讀者的困惑,我們稱執行期間所發生的事為*動態載入*,這一主題會在下一章節中談到。你也會在別的地方看到我把動態載入描述成*動態連結*,不過不會是在這一章節中。換句話說,這一章節所談的,全部是指發生在編譯結束後的連結。

6.1 共享程式庫 vs靜態程式庫

建立程式的最後一個步驟便是連結;也就是將所有分散的小程式組合起來,看看是否遺漏了些什麼。顯然,有一些事情是很多程式都會想做的---例如,開啟檔案,接著所有與開檔有關的小程式就會將儲存程式庫的相關檔案提供給你的程式使用。在一般的Linux系統上,這些小程式可以在/lib/usr/lib/目錄底下找到。

當你用的是靜態的程式庫時,連結器會找出程式所需的模組,然後實際將它們拷貝到執行檔內。然而,對共享程式庫而言,就不是這樣了。共享程式庫會在執行檔內留下一個記號,指明*當程式執行時,首先必須載入這個程式庫*。顯然,共享程式庫是試圖使執行檔變得更小,等同於使用更少的記憶體與磁碟空間。Linux內定的行為是連結共享程式庫,只要Linux能找到這些共享程式庫的話,就沒什麼問題;不然,Linux就會連結靜態的了。如果你想要共享程式庫的話,檢查這些程式庫(*.sa for a.out, *.so for ELF)是否住在它們該在的地方,而且是可讀取的。

在Linux上,靜態程式庫會有類似libname.a這樣的名稱;而共享程式庫則稱為libname.so.x.y.z,此處的x.y.z是指版本序號的樣式。共享程式庫通常都會有連結符號指向靜態程式庫(很重要的)與相關聯的.sa檔案。標準的程式庫會包含共享與靜態程式庫兩種格式。

你可以用ldd(List Dynamic Dependencies)來查出某支程式需要哪些共享程式庫。

$ ldd /usr/bin/lynx
        libncurses.so.1 => /usr/lib/libncurses.so.1.9.6
        libc.so.5 => /lib/libc.so.5.2.18

這是說在我的系統上,WWW瀏覽器*lynx*會依賴libc.so.5 (the C library)與libncurses.so.1(終端機螢幕的控制)的存在。若某支程式缺乏獨立性, ldd就會說‘statically linked’或是‘statically linked (ELF)’。

6.2 終極審判(‘sin() 在哪個程式庫裡?’)

nm 程式庫名稱應該會列出此程式庫名稱所參考到的所有符號。這個指令可以應用在靜態與共享程式庫上。假設你想知道tcgetattr()是在哪兒定義的:你可以如此做,

$ nm libncurses.so.1 |grep tcget
         U tcgetattr

*U*指出*未定義*---也就是說ncurses程式庫有用到tegetattr(),但是並沒有定義它。你也可以這樣做,

$ nm libc.so.5 | grep tcget
00010fe8 T __tcgetattr
00010fe8 W tcgetattr
00068718 T tcgetpgrp

*W*說明了*弱態(weak)*,意指符號雖已定義,但可由不同程式庫中的另一定義所替代。最簡單的*正常*定義(像是tcgetpgrp)是由*T*所標示:

標題所談的問題,最簡明的答案便是libm.(so|a)了。所有定義在<math.h>的函數都保留在maths程式庫內;因此,當你用到其中任何一個函數時,都需要以-lm的參數連結此程式庫。

6.3 X檔案?

ld: Output file requires shared library `libfoo.so.1`

ld與其相類似的命令在搜尋檔案的策略上,會依據版本的差異而有所不同,但是唯一一個你可以合理假設的內定目錄便是/usr/lib了。如果你希望身處它處的程式庫也列入搜尋的行列中,那麼你就必須以-L選項告知gcc或是ld。

要是你發現一點效果也沒有,就趕緊察看看那檔案是不是還乖乖的躺在原地。就a.out而言,以-lfoo參數來連結,會驅使ld去尋找libfoo.sa(shared stubs);如果沒有成功,就會換成尋找libfoo.a(static)。就ELF而言, ld會先找libfoo.so,然後是libfoo.alibfoo.so通常是一個連結符號,連結至libfoo.so.x

6.4 建立你自己的程式庫

控制版本

與其它任何的程式一樣,程式庫也有修正不完的bugs的問題存在。它們也可能產生出一些新的特點,更改目前存在的模組的功效,或是將舊的移除掉。這對正在使用它們的程式而言,可能會是一個大問題。如果有一支程式是根據那些舊的特點來執行的話,那怎麼辦?

所以,我們引進了程式庫版本編號的觀念。我們將程式庫*次要*與*主要*的變更分門別類,同時規定*次要*的變更是不允許用到這程式庫的舊程式發生中斷的現象。你可以從程式庫的檔名分辨出它的版本(實際上,嚴格來講,對ELF而言僅僅是一場天大的謊言;繼續讀將下去,便可明白為什麼了): libfoo.so.1.2的主要版本是1,次要版本是2。次要版本的編號可能真有其事,也可能什麼都沒有---libc在這一點上用了*修正程度*的觀念,而訂出了像libc.so.5.2.18這樣的程式庫名稱。次要版本的編號內若是放一些字母、底線、或是任何可以列印的ASCII字元,也是很合理的。

ELF與a.out格式最主要的差別之一就是在設置共享程式庫這件事上;我們先看ELF,因為它比較簡單一些。

ELF?它到底是什麼東東ㄋㄟ?

ELF(Executable and Linking Format)最初是由USL(UNIX System Laboratories)發展而成的二進位格式,目前正應用於Solaris與System V Release 4上。由於ELF所增漲的彈性遠遠超過Linux過去所用的a.out格式,因此GCC與C程式庫的發展人士於1995年決定改用ELF為Linux標準的二進位格式。

怎麼又來了?

這一節是來自於‘/news-archives/comp.sys.sun.misc’的文件。

ELF(“Executable Linking Format”)是於SVR4所引進的新式改良目的檔格式。ELF比起COFF可是多出了不少的功能。以ELF而言,它*是*可由使用者自行延伸的。ELF視一目的檔為節區(sections),如串列般的組合;而且此串列可為任意的長度(而不是一固定大小的陣列)。這些節區與COFF的不一樣,並不需要固定在某個地方,也不需要以某種順序排列。如果使用者希望補捉到新的資料,便可以加入新的節區到目的檔內。ELF也有一個更強而有力的除錯法式,稱為DWARF(Debugging With Attribute Record Format)—目前Linux並不完全支援。DWARF DIEs(Debugging Information Entries)的連結串列會在ELF內形成 .debug的節區。DWARF DIEs的每一個 .debug節區並非一些少量且固定大小的資訊記錄的集合,而是一任意長度的串列,擁有複雜的屬性,而且程式的資料會以有範圍限制的樹狀資料結構寫出來。DIEs所能補捉到的大量資訊是COFF的 .debug節區無法望其項背的。(像是C++的繼承圖。)

ELF檔案是從SVR4(Solaris 2.0 ?)ELF存取程式庫(ELF access library)內存取的。此程式庫可提供一簡便快速的介面予ELF。使用ELF存取程式庫最主要的恩惠之一便是,你不再需要去察看一個ELF檔的qua了。就UNIX的檔案而言,它是以Elf*的型式來存取;呼叫elf_open()之後,從此時開始,你只需呼叫elf_foobar()來處理檔案的某一部份即可,並不需要把檔案實際在磁碟上的image搞得一團亂。

ELF的優缺點與昇級至ELF等級所需經歷的種種痛苦,已在ELF-HOWTO內論及;我並不打算在這兒塗漿糊。ELF HOWTO應該與這份文件有同樣的主題才是。

ELF共享程式庫

若想讓libfoo.so成為共享程式庫,基本的步驟會像下面這樣:

$ gcc -fPIC -c *.c
$ gcc -shared -Wl,-soname,libfoo.so.1 -o libfoo.so.1.0 *.o
$ ln -s libfoo.so.1.0 libfoo.so.1
$ ln -s libfoo.so.1 libfoo.so
$ LD_LIBRARY_PATH=`pwd`:$LD_LIBRARY_PATH ; export LD_LIBRARY_PATH

這會產生一個名為libfoo.so.1.0的共享程式庫,以及給予ld適當的連結(libfoo.so)還有使得動態載入程式(dynamic loader)能找到它(libfoo.so.1)。為了進行測試,我們將目前的目錄加到LD_LIBRARY_PATH裡。

當你津津樂道於程式庫製做成功之時,別忘了把它移到如/usr/local/lib的目錄底下,並且重新設定正確的連結路徑。libfoo.so.1libfoo.so.1.0的連結會由ldconfig依日期不斷的更新,就大部份的系統來說,ldconfig會在開機過程中執行。libfoo.so的連結必須由手動方式更新。如果你對程式庫所有組成份子(如標頭檔等)的昇級,總是抱持著一絲不茍的態度,那麼最簡單的方法就是讓libfoo.so -> libfoo.so.1;如此一來,ldconfig便會替你同時保留最新的連結。要是你沒有這麼做,你自行設定的東東就會在數日後造成千奇百怪的問題出現。到時候,可別說我沒提醒你啊!

$ su
# cp libfoo.so.1.0 /usr/local/lib
# /sbin/ldconfig
# ( cd /usr/local/lib ; ln -s libfoo.so.1 libfoo.so )

版本編號、soname與符號連結

每一個程式庫都有一個soname。當連結器發現它正在搜尋的程式庫中有這樣的一個名稱,連結器便會將soname箝入連結中的二進位檔內,而不是它正在運作的實際的檔名。在程式執行期間,動態載入程式會搜尋擁有soname這樣的檔名的檔案,而不是程式庫的檔名。因此,一個名為libfoo.so的程式庫,就可以有一個libbar.so的soname了。而且所有連結到libbar.so的程式,當程式開始執行時,會尋找的便是libbar.so了。

這聽起來好像一點意義也沒有,但是這一點,對於瞭解數個不同版本的同一個程式庫是如何在單一系統上共存的原因,卻是關鍵之鑰。Linux程式庫標準的命名方式,比如說是libfoo.so.1.2,而且給這個程式庫一個libfoo.so.1的soname。如果此程式庫是加到標準程式庫的目錄底下(e.g. /usr/lib),ldconfig會建立符號連結libfoo.so.1 -> libfoo.so.1.2,使其正確的image能於執行期間找到。你也需要連結libfoo.so -> libfoo.so.1,使ld能於連結期間找到正確的soname。

所以囉,當你修正程式庫內的bugs,或是添加了新的函數進去(任何不會對現存的程式造成不利的影響的改變),你會重建此程式庫,保留原本已有的soname,然後更改程式庫檔名。當你對程式庫的變更會使得現有的程式中斷,那麼你只需增加soname中的編號---此例中,稱新版本為libfoo.so.2.0,而soname變成libfoo.so.2。緊接著,再將libfoo.so的連結轉向新的版本;至此,世界又再度恢復了和平!

其實你不須要以此種方式來替程式庫命名,不過這的確是個好的傳統。ELF賦予你在程式庫命名上的彈性,會使得人氣喘呼呼的搞不清楚狀況;有這樣的彈性在,也並不表示你就得去用它。

ELF總結:假設經由你睿智的觀察發現有個慣例說:程式庫主要的昇級會破壞相容性;而次要的昇級則可能不會;那麼以下面的方式來連結,所有的一切就都會相安無事了。

gcc -shared -Wl,-soname,libfoo.so.major -o libfoo.so.major.minor

a.out---舊舊的格式﹏

建立共享程式庫的便利性是昇級至ELF的主要原因之一。那也是說,a.out可能還是有用處在的。上ftp站去抓 ftp://tsx-11.mit.edu/pub/linux/packages/GCC/src/tools-2.17.tar.gz;解壓縮後你會發現有20頁的文件可以慢慢的讀哩。我很不喜歡自己黨派的偏見表現得那麼的淋璃盡致,可是從上下文間,應該也可以很清楚的嗅出我從來不拿石頭砸自己的腳的脾氣吧!:-)

ZMAGIC vs QMAGIC

QMAGIC是一種類似舊格式的a.out(亦稱為ZMAGIC)的可執行檔 格式,這種格式會使得第一個分頁無法map。當0-4096的範圍內沒有mapping存在時,則可允許NULL dereference trapping更加的容易。所產生的邊界效應是你的執行檔會比較小(大約少1K左右)。

只有即將作廢的連結器有支援ZMAGIC,一半已埋入棺材的連結器有支援這兩種格式;而目前的版本僅支援QMAGIC而已。事實上,這並沒有多大的影響,那是因為目前的核心兩種格式都能執行。

*file*命令應該可以確認程式是不是QMAGIC的格式的。

檔案配置

一a.out(DLL)的共享程式庫包含兩個真實的檔案與一個連結符號。就*foo*這個用於整份文件做為範例的程式庫而言,這些檔案會是libfoo.salibfoo.so.1.2;連結符號會是libfoo.so.1,而且會指向libfoo.so.1.2。這些是做什麼用的?

在編譯時,ld會尋找libfoo.sa。這是程式庫的*stub*檔案。而且含有所有執行期間連結所需的exported的資料與指向函數的指標。

執行期間,動態載入程式會尋找libfoo.so.1。這僅僅是一個符號連結,而不是真實的檔案。故程式庫可更新成較新的且已修正錯誤的版本,而不會損毀任何此時正在使用此程式庫的應用程式。在新版---比如說libfoo.so.1.3---已完整呈現時,ldconfig會以一極微小的操作,將連結指向新的版本,使得任何原本使用舊版的程式不會感到絲毫的不悅。

DLL程式庫(我知道這是無謂的反覆---所以對我提出訴訟吧!)通常會比它們的靜態副本要來得大多。它們是以*洞(holes)*的形式來保留空間以便日後的擴充。這種*洞*可以不佔用任何的磁碟空間。一個簡單的cp呼叫,或是使用makehole程式,就可以達到這樣效果。因為它們的位址是固定在同一位置上,所以在建立程式庫後,你可以把它們拿掉。不過,千萬不要試著拿掉ELF的程式庫

``libc-lite''?

libc-lite是輕量級的libc版本。可用來存放在磁碟片上,也可以替大部份低微的UNIX任務收尾。它沒有包含curses, dbm, termcap等等的程式碼。如果你的/lib/libc.so.4是連結到一個lite的libc,那麼建議你以完整的版本取代它。

連結:常見的問題

把你連結時所遭遇的問題寄給我!我可能什麼事也不會做,但是只要累積了足夠的數量,我會把它們寫起來*。

你想共享,偏偏程式卻連結成靜態的!

檢查你提供給ld的連結是否正確,使ld能找到每一個對應的共享程式庫,就ELF而言,這是指一個符號連結libfoo.so,連結至image;就a.out而言,就是libfoo.sa檔了。很多人將ELF binutils 2.5昇級至2.6之後,就產生了這個問題---早期的版本搜尋共享程式庫時較有智慧,所以並沒有將所有的連結建立起來。後來,為了與其它的架構相容,這項充滿智慧的行為被人給刪除掉了,另外,這樣的*智慧*判斷錯誤的機率相當高,所造成的麻煩比它所解決的問題還多,所以留著也是害人精;不如歸去兮!

DLL的工具程式‘mkimage’找不到libgcc?

libc.so.4.5.x之後,libgcc已不再是共享的格式。因此,你必須在*-lgcc*出現之處以`gcc -print-libgcc-file-name`替代(完整的倒單引號(back-quotes))。另外,刪除所有/usr/lib/libgcc*的檔案。這點很重要哩。

__NEEDS_SHRLIB_libc_4 multiply defined messages

是同樣的問題所造成的另一種結果。

``Assertion failure'' message when rebuilding a DLL ?

這一條神秘的訊息最有可能的原因是,在原始的jump.vars檔案內,由於保留的空間太少,以致於造成其中一個jump table slots溢滿。你可以執行工具程式—由2.17.tar.gz套件所提供的‘getsize’命令,定出所有嫌疑犯的蹤跡。可能唯一的解決方法是,解除此程式庫主要的版本編號,強迫它回到不相容的年代。

ld: output file needs shared library libc.so.4

通常這是發生在當你連結的程式庫不是libc(如X程式庫),而且在命令列用了-g的參數,卻沒有一併使用-static,所發出的錯誤訊息。

共享程式庫的.sa stubs通常有一個未定義的符號_NEEDS_SHRLIB_libc_4;這一點可藉由libc.sa stub來解決,然而,以-g來編譯時,會使得連結以libg.alibc.a來結束;因此這個符號一直就沒有解決,也就會導致上面的錯誤訊息了。

總之,以-g的旗號編譯時別忘了加上-static,不然就別用-g來連結。通常,以-g編譯各個獨立的檔案時,所獲得的除錯資訊已經足夠,連結時就可以不需要它了。


Next Previous Contents