linux編譯時和執行時,庫搜尋路徑和順序

查志強發表於2014-06-21
【原文:http://linux.chinaunix.net/techdoc/develop/2008/12/26/1054992.shtml
轉自:
http://docs.sun.com/app/docs/doc/820-1204/aeudh?l=zh&a=view
http://docs.huihoo.com/gnu/gcc/gcc_howto/GCC-HOWTO-6.html
有改動。
LD_LIBRARY_PATH 環境變數
使用 LD_LIBRARY_PATH 環境變數指定連結程式應在哪些目錄路徑中搜尋用 -llibrary 選項指定的庫。
可以指定多個目錄,其間用冒號分隔。通常,LD_LIBRARY_PATH 變數包含兩個用冒號分隔的目錄列表,列表間用分號隔開:
dirlist1;dirlist2 
首先搜尋 dirlist1 中的目錄,接著是命令列上用任何顯式 -Ldir 指定的目錄,再接著是 dirlist2 以及標準目錄。
也就是說,如果使用多個 -L 呼叫編譯器,如下所示:
f95 ... -Lpath1 ... -Lpathn ... 
則搜尋順序是:
dirlist1 path1 ... pathn dirlist2 standard_paths 
當 LD_LIBRARY_PATH 變數只包含一個用冒號分隔的目錄列表時,它會被解釋為 dirlist2。
注 – 


  • 強烈建議不要在編譯軟體使用 LD_LIBRARY_PATH 環境變數。儘管它作為一種影響連結程式搜尋路徑的臨時機制很有用,包含該環境變數的終端要執行的要動態連結的可執行程式的搜尋路徑都會被改變。您可能會看到了意想不到的結果或效能降低。


. 庫搜尋路徑和順序-靜態連結
使用 -llibrary 編譯器選項對連結程式在解析外部引用時要搜尋的其他庫命名。例如,用選項 -lmylib 將庫 libmylib.so 或 libmylib.a 新增到搜尋列表中。
連結程式會在標準目錄路徑中查詢其他的 libmylib 庫。-L 選項(和 LD_LIBRARY_PATH 環境變數)會建立一個路徑列表,告知連結程式到哪裡查詢位於標準路徑以外的庫。
假如 libmylib.a 位於 /home/proj/libs 目錄中,則選項 –L/home/proj/libs 會告知連結程式在生成可執行檔案時到哪裡查詢:
demo% f95 -o pgram part1.o part2.o -L/home/proj/libs -lmylib
-l library 選項的命令列順序
對於任何未解析的特殊引用,只對庫進行一次搜尋,並且只搜尋在搜尋時未定義的符號。如果命令列上列出了多個庫,則會按其在命令列上出現的順序來搜尋這些庫。-llibrary 選項放置在以下位置:


  • 將 -llibrary 選項放置在任一 .f.for.F.f95 或 .o 檔案之後。

  • 如果呼叫了 libx 中的函式,並且這些函式引用了 liby 中的函式,則將 -lx 置於 -ly 之前。

-Ldir 選項的命令列順序
-Ldir 選項會將 dir 目錄路徑新增到庫搜尋列表中。連結程式首先在 -L 選項指定的任何目錄中搜尋庫,然後在標準目錄中進行搜尋。只有將其放在它所應用的 –llibrary 選項之前,該選項才有用。
. 庫搜尋路徑和順序-動態連結
對於動態庫,庫搜尋路徑和載入順序的更改與靜態情況不同。實際連結發生在執行時而不是編譯時。
. 在編譯時指定動態庫
編譯可執行檔案時,連結程式會在可執行檔案本身中記錄共享庫的路徑。這些搜尋路徑可以用 -Rpath 選項指定。這一點與 -Ldir 選項相反,該選項在編譯時指示到哪裡查詢 -llibrary 選項所指定的庫,但不會將該路徑記錄到二進位制可執行檔案中。
使用 dump 命令可以檢視建立可執行檔案時內建的目錄路徑。
示例:列出內建於 a.out 之中的目錄路徑:
demo% f95 program.f -R/home/proj/libs -L/home/proj/libs -lmylib
demo% dump -Lv a.out | grep RPATH
[5]      RPATH    /home/proj/libs:/opt/SUNWspro/lib
. 在執行時指定動態庫
執行時,連結程式會確定到哪裡查詢可執行檔案所需的動態庫:


  • 執行時 LD_LIBRARY_PATH 的值

  • 生成可執行檔案時已由 -R 指定的路徑

如前所述,使用 LD_LIBRARY_PATH 會帶來意想不到的副作用,因而不建議這樣做。
. 修復動態連結期間的錯誤
當動態連結程式找不到所需庫的位置時,它會發出以下錯誤訊息:
ld.so: prog: fatal: libmylib.so: can’t open file:
此訊息表明這些庫不在其應在的位置。
使用 ldd 確定可執行檔案期望在哪兒找到這些庫:
demo% ldd a.out
libfui.so.1 =>   /opt/SUNWspro/lib/libfui.so.1
    libfai.so.1 =>   /opt/SUNWspro/lib/libfai.so.1
    libfai2.so.1 =>  /opt/SUNWspro/lib/libfai2.so.1
    libfsumai.so.1 =>    /opt/SUNWspro/lib/libfsumai.so.1
    libfprodai.so.1 =>   /opt/SUNWspro/lib/libfprodai.so.1
    libfminlai.so.1 =>   /opt/SUNWspro/lib/libfminlai.so.1
    libfmaxlai.so.1 =>   /opt/SUNWspro/lib/libfmaxlai.so.1
    libfminvai.so.1 =>   /opt/SUNWspro/lib/libfminvai.so.1
    libfmaxvai.so.1 =>   /opt/SUNWspro/lib/libfmaxvai.so.1
    libfsu.so.1 =>   /opt/SUNWspro/lib/libfsu.so.1
    libsunmath.so.1 =>   /opt/SUNWspro/lib/libsunmath.so.1
    libm.so.1 =>     /usr/lib/libm.so.1
    libc.so.1 =>     /usr/lib/libc.so.1
    libdl.so.1 =>    /usr/lib/libdl.so.1
    /usr/platform/SUNW,Ultra-5_10/lib/libc_psr.so.1
如果可能的話,將這些庫移動或複製到正確的目錄中,或者在連結程式搜尋的目錄中建立到該目錄的軟連結(使用 ln -s)。或者,可能是沒有正確設定 LD_LIBRARY_PATH。檢查 LD_LIBRARY_PATH 是否包含執行時所需庫的路徑。
///////////////////////////////////////////////////////////
. 建立你自己的程式庫 
控制版本
與其它任何的程式一樣,程式庫也有修正不完的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.1與libfoo.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站去抓 
[url=javascript:if(confirm('ftp://tsx-11.mit.edu/pub/linux/packages/GCC/src/tools-2.17.tar.gz  \n\nThis file was not retrieved by Teleport Pro, because it is addressed on a domain or path outside the boundaries set for its Starting Address.  \n\nDo you want to open it from the server?'))window.location='ftp://tsx-11.mit.edu/pub/linux/packages/GCC/src/tools-2.17.tar.gz']ftp://tsx-11.mit.edu/pub/linux/packages/GCC/src/tools-2.17.tar.gz[/url]
;解壓縮後你會發現有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.sa與libfoo.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.a或libc.a來結束;因此這個符號一直就沒有解決,也就會導致上面的錯誤訊息了。 
總之,以-g的旗號編譯時別忘了加上-static,不然就別用-g來連結。通常,以-g編譯各個獨立的檔案時,所獲得的除錯資訊已經足夠,連結時就可以不需要它了。 
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*所標示: 
注意:以上是一般的ld搜尋方式,編譯程式時還會碰到一種情況:在ld搜尋到.so檔案但該檔案很小,裡面是ld script語句,又將ld 指向了別處,這種情況如果ld script 中有絕對路徑就容易出問題,所以在庫檔案很小時(百十k左右),就要開啟看看了,如果引起問題可以將該含有ld script的so檔案刪除,建立一個指向正確庫檔案的連線來替代它。

相關文章