深入理解動態(tài)鏈接

動態(tài)鏈接庫又叫共享庫(Shared Library)芦圾,相信大部分做軟件開發(fā)的人都很熟悉囊扳。簡單地說瓤荔,庫是對一系列程序的封裝挥转,靜態(tài)庫是會在鏈接時與可執(zhí)行程序合并的庫海蔽,而動態(tài)庫則在鏈接后仍然與可執(zhí)行文件分離,直到運行時才動態(tài)加載绑谣。顯然党窜,動態(tài)庫可以共享給多個可執(zhí)行程序同時使用,更節(jié)約硬盤和內存空間借宵。

不管是Windows開發(fā)者幌衣,還是Linux開發(fā)者,或者是Android壤玫、iOS開發(fā)者豁护,我們無時無刻都在生產或者使用動態(tài)庫,而且很少遇到困難欲间。這得益于一套完整的動態(tài)鏈接機制楚里,該機制保證鏈接和運行時,能夠準確找到正確的動態(tài)庫括改。本文就來探討動態(tài)鏈接的內部機制腻豌。

一個簡單的案例

看看下面這個簡單到不能再簡單的例子。有一個main.cpp文件嘱能,用來生成可執(zhí)行程序。

// main.cpp
#include "random.h"

int main() {
    return get_random_number();
}

該程序依賴于一個random庫虱疏,庫的源碼如下:

// random.h
int get_random_number();
// random.cpp
#include "random.h"

int get_random_number(void) {
    return 4;
}

現(xiàn)在惹骂,我們用clang++編譯器編譯這個程序。(clang++與g++類似做瞪,但更適合于開發(fā)对粪,可以sudo apt install clang安裝。)

先編譯random這個動態(tài)鏈接庫:

$ clang++ -o random.o -c random.cpp

其中装蓬,-o指定輸出文件的名稱著拭,我們把源文件random.cpp編譯成目標文件random.o-c表示只編譯牍帚、不鏈接儡遮。然后再把目標文件編譯成動態(tài)庫:

$ clang++ -shared -o librandom.so random.o

其中,-shared表示生成動態(tài)鏈接庫而不是靜態(tài)庫暗赶,-o指定輸出文件名為librandom.so鄙币。注意該名稱不是隨便定的肃叶,而是遵守了動態(tài)鏈接的慣例——所有庫的命名形式都為lib<name>.so

接下來十嘿,我們編譯可執(zhí)行程序因惭,首先生成目標文件main.o

$ clang++ -o main.o -c main.cpp

然后生成可執(zhí)行文件main

$ clang++ -o main main.o
main.o:在函數‘main’中:
main.cpp:(.text+0x10):對‘get_random_number()’未定義的引用
clang: error: linker command failed with exit code 1 (use -v to see invocation)

不出意外地出錯了,因為我們沒有鏈接到random庫绩衷,所以出現(xiàn)“未定義的引用”蹦魔。

Tips:這里給個小提示,在開發(fā)C++程序的時候咳燕,只要看到錯誤信息“未定義的引用”版姑,一定是某個庫忘記鏈接了,如果用的CMake迟郎,很有可能是target_link_libraries里面少寫了某個依賴項剥险,或者即便寫了,但是拼寫有誤宪肖,像是把${PROTOBUF_LIBRARIES}寫成${Protobuf_LIBRARIES}甚至是寫成${PROTOBUF_LIBS}而出錯的情況可是層出不窮表制。

這次我們指定需要鏈接的庫:

$ clang++ -o main main.o -lrandom
/usr/bin/ld: 找不到 -lrandom
clang: error: linker command failed with exit code 1 (use -v to see invocation)

又出錯了。倒也不難想象控乾,雖然我們指定了庫的名稱么介,但并沒有指定庫的路徑,鏈接器/usr/bin/ld不知道去哪里找random這個庫蜕衡。所以我們得指定搜索路徑壤短。

$ clang++ -o main main.o -lrandom -L.

其中-L后面緊跟著路徑.,表示在當前目錄下查找?guī)臁慨仿,F(xiàn)在久脯,終于編譯成功了,讓我們運行main這個程序:

$ ./main
./main: error while loading shared libraries: librandom.so: cannot open shared object file: No such file or directory

好了镰吆,這又是一個常見的錯誤帘撰。當我們滿心歡喜準備見證運行結果的時候,它卻告訴我們程序根本不能運行万皿。這是因為動態(tài)鏈接的可執(zhí)行程序在運行前摧找,需要先加載所需的動態(tài)鏈接庫。雖然我們剛剛用-L.指定了鏈接路徑牢硅,但該路徑只對編譯期生效蹬耘。為了弄清楚運行時動態(tài)鏈接的方式,我們需要更加深入减余。

ELF文件格式

簡單來說综苔,目前Linux系統(tǒng)上的大部分的可執(zhí)行文件和庫文件都是ELF格式(Executable Linkable Format)。與此對應,Windows系統(tǒng)上的可執(zhí)行文件和庫文件是PE格式(Portable Executable)休里。這些格式定義了可執(zhí)行文件的二進制結構蛆挫。通常我們會認為可執(zhí)行文件里面包含的無非是二進制代碼和數據,但其實還包含了其它信息妙黍,比如架構信息悴侵、大小端模式、調試信息拭嫁、動態(tài)鏈接信息等等可免。這里我們只關注ELF中與動態(tài)鏈接相關的信息,執(zhí)行以下代碼:

$ readelf -d main
Dynamic section at offset 0xde8 contains 28 entries:
  標記        類型                         名稱/值
 0x0000000000000001 (NEEDED)             共享庫:[librandom.so]
 0x0000000000000001 (NEEDED)             共享庫:[libstdc++.so.6]
 0x0000000000000001 (NEEDED)             共享庫:[libm.so.6]
 0x0000000000000001 (NEEDED)             共享庫:[libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             共享庫:[libc.so.6]
 0x000000000000000c (INIT)               0x400568
 0x000000000000000d (FINI)               0x400764
 0x0000000000000019 (INIT_ARRAY)         0x600dd0
 0x000000000000001b (INIT_ARRAYSZ)       8 (bytes)
 0x000000000000001a (FINI_ARRAY)         0x600dd8
 0x000000000000001c (FINI_ARRAYSZ)       8 (bytes)
 0x000000006ffffef5 (GNU_HASH)           0x400298
 0x0000000000000005 (STRTAB)             0x4003f0
 0x0000000000000006 (SYMTAB)             0x4002d0
 0x000000000000000a (STRSZ)              241 (bytes)
 0x000000000000000b (SYMENT)             24 (bytes)
 0x0000000000000015 (DEBUG)              0x0
 0x0000000000000003 (PLTGOT)             0x601000
 0x0000000000000002 (PLTRELSZ)           48 (bytes)
 0x0000000000000014 (PLTREL)             RELA
 0x0000000000000017 (JMPREL)             0x400538
 0x0000000000000007 (RELA)               0x400520
 0x0000000000000008 (RELASZ)             24 (bytes)
 0x0000000000000009 (RELAENT)            24 (bytes)
 0x000000006ffffffe (VERNEED)            0x400500
 0x000000006fffffff (VERNEEDNUM)         1
 0x000000006ffffff0 (VERSYM)             0x4004e2
 0x0000000000000000 (NULL)               0x0

readelf是一個查看ELF格式文件信息的工具做粤,-d表示查看動態(tài)鏈接信息浇借。其中,前幾行列出了main依賴的動態(tài)鏈接庫怕品,總共有5個妇垢。除了我們編譯時手動指定的librandom之外,還有libstdc++標準C++庫肉康、libm基礎數學庫闯估、libgcc_sGCC運行時庫、libc系統(tǒng)調用庫吼和,這些庫是編譯器自動為每個可執(zhí)行程序添加的涨薪。

從上面的結果可以看出,可執(zhí)行程序會記錄每一個它所需要的動態(tài)庫的名稱炫乓,但似乎沒有記錄這些動態(tài)庫的路徑刚夺,至少在這個例子中沒有。不過末捣,當我們調用./main時侠姑,除了random庫兵多,其它4個庫顯然是可以鏈接成功的萧吠。我們如何才能知道運行時實際的鏈接過程呢?

好在另一個工具可以預覽運行時的鏈接信息:

$ ldd main
    linux-vdso.so.1 =>  (0x00007ffcdde26000)
    librandom.so => not found
    libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f74aaf37000)
    libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f74aac2e000)
    libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f74aaa18000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f74aa64e000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f74ab2b9000)

果然衅胀,除了random卒茬,其它庫都鏈接到了正確的位置】欤可ldd是如何找到這些路徑的呢圃酵?這就依賴于動態(tài)鏈接機制中的一項規(guī)定,鏈接器會按照如下的順序在指定目錄中查找所需的動態(tài)鏈接庫:

  1. ELF的rpath中規(guī)定的路徑馍管。
  2. LD_LIBRARY_PATH環(huán)境變量中的路徑郭赐。
  3. ELF的runpath中規(guī)定的路徑。
  4. /etc/ld.so.conf文件中列出的路徑。該文件可包含子文件捌锭,因此也包括子文件中列出的路徑俘陷。
  5. 默認的系統(tǒng)路徑/lib/usr/lib

鏈接器找不到random庫观谦,說明它并不在以上這些路徑中拉盾。最簡單的做法,我們可以把random的路徑添加到LD_LIBRARY_PATH環(huán)境變量中豁状。

$ LD_LIBRARY_PATH=.
$ ./main

終于捉偏,我們的main正常執(zhí)行了。但這種解決方式還不夠優(yōu)雅泻红,讓我們回想一下平常使用的程序是怎么運行的夭禽。以OpenCV為例,通常有兩種安裝方式谊路,通過apt install安裝或編譯安裝讹躯。如果是apt install安裝,會自動把庫安裝到系統(tǒng)目錄/usr/lib下缠劝,這種情況鏈接器可以直接找到它們潮梯。如果編譯安裝,安裝路徑就由用戶自己指定了剩彬,默認會安裝到/usr/local酷麦,當然也可以安裝到其它任何位置。這種情況喉恋,依賴于OpenCV的程序如何能夠找到這些庫呢沃饶?我通常用CMake編譯程序,在CMakeLists.txt中可以指定依賴庫的路徑轻黑,并用find_package找到這些庫糊肤。如此編譯得到的可執(zhí)行程序是可以直接運行的,我們對照上面的動態(tài)鏈接庫查找路徑氓鄙,顯然不是在2馆揉、4、5中找到的升酣,只能是在1或2中找到的。現(xiàn)在我們就來看看rpathrunpath到底是什么态罪。

rpath和runpath

與其它搜索路徑不同噩茄,rpathrunpath是直接保存在ELF文件中的,可以在編譯可執(zhí)行程序時設置該路徑≡淦校現(xiàn)在机杜,我們重新編譯main,把rpath加進去衅谷。

$ clang++ -o main main.o -lrandom -L. -Wl,-rpath,.

其中椒拗,-Wl后面跟著逗號分隔的鏈接器參數-rpath.,意思是告訴鏈接器參數-rpath的值是.会喝,也就是當前路徑《傅現(xiàn)在,不必修改環(huán)境變量肢执,直接調用./main也可以正常運行了枉阵。如果我們再查看一下ELF中的詳細信息:

$ readelf -d main
Dynamic section at offset 0xdd8 contains 29 entries:
  標記        類型                         名稱/值
 0x0000000000000001 (NEEDED)             共享庫:[librandom.so]
 0x0000000000000001 (NEEDED)             共享庫:[libstdc++.so.6]
 0x0000000000000001 (NEEDED)             共享庫:[libm.so.6]
 0x0000000000000001 (NEEDED)             共享庫:[libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             共享庫:[libc.so.6]
 0x000000000000000f (RPATH)              Library rpath: [.]
 ...

可以發(fā)現(xiàn)多了一項RPATH,正是我們剛剛設置的值预茄。rpathrunpath的區(qū)別僅僅是優(yōu)先級不同兴溜,runpath中的路徑可以被外部的環(huán)境變量LD_LIBRARY_PATH覆蓋,而rpath則不會耻陕。

現(xiàn)實場景

文中的例子很易于理解拙徽,但畢竟有些不切實際,沒人會手動編譯源文件诗宣,也不會手動設置rpath膘怕。對于Linux上的開發(fā)者而言,CMake可以說是最常用的構建工具召庞。當我們寫好CMakeLists.txt后岛心,CMake會幫我們設置好鏈接庫的名稱,以及rpath等搜索路徑篮灼。此外忘古,CMake提供了一些命令用來手動設置rpath,但我們一般都不需要用诅诱,這里就不提了髓堪。值得一提的是,CMake會根據構建類型來決定是否設置rpath娘荡,在build時干旁,會添加庫路徑到rpath,而在install時炮沐,則會把rpath設置為空疤孕。這是因為,install之后央拖,認為可執(zhí)行文件是可移植的,不必依賴于編譯時鏈接的特定的庫,庫的搜索路徑完全由所在系統(tǒng)的默認庫路徑和環(huán)境變量決定鲜戒。

到這里专控,本文可以告一段落了。我們了解了動態(tài)鏈接的原理和方式遏餐,雖然并不十分深入伦腐,但至少懂得了編譯器和鏈接器是如何工作的,了解了常見的鏈接錯誤出現(xiàn)的原因及其解決方案失都。

需要特別提出的是柏蘑,本文并非原創(chuàng),而是參考了一篇博客Shared Libraries: Understanding Dynamic Loading的內容粹庞,并做了適當的簡化咳焚。原文中有更詳細的內容,建議感興趣的同學去讀一讀庞溜。在此對原作者Amir Rachum表示感謝革半。

參考資料

Shared Libraries: Understanding Dynamic Loading Amir Rachum
Executable and Linkable Format Wikipedia
深入理解程序構造(一) 卡巴拉的樹
Rpath handling CMake Community Wiki

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市流码,隨后出現(xiàn)的幾起案子又官,更是在濱河造成了極大的恐慌,老刑警劉巖漫试,帶你破解...
    沈念sama閱讀 212,185評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件六敬,死亡現(xiàn)場離奇詭異,居然都是意外死亡驾荣,警方通過查閱死者的電腦和手機外构,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,445評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來秘车,“玉大人典勇,你說我怎么就攤上這事《E浚” “怎么了割笙?”我有些...
    開封第一講書人閱讀 157,684評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長眯亦。 經常有香客問我伤溉,道長,這世上最難降的妖魔是什么妻率? 我笑而不...
    開封第一講書人閱讀 56,564評論 1 284
  • 正文 為了忘掉前任乱顾,我火速辦了婚禮,結果婚禮上宫静,老公的妹妹穿的比我還像新娘走净。我一直安慰自己券时,他們只是感情好,可當我...
    茶點故事閱讀 65,681評論 6 386
  • 文/花漫 我一把揭開白布伏伯。 她就那樣靜靜地躺著橘洞,像睡著了一般。 火紅的嫁衣襯著肌膚如雪说搅。 梳的紋絲不亂的頭發(fā)上炸枣,一...
    開封第一講書人閱讀 49,874評論 1 290
  • 那天,我揣著相機與錄音弄唧,去河邊找鬼适肠。 笑死,一個胖子當著我的面吹牛候引,可吹牛的內容都是我干的侯养。 我是一名探鬼主播,決...
    沈念sama閱讀 39,025評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼背伴,長吁一口氣:“原來是場噩夢啊……” “哼沸毁!你這毒婦竟也來了?” 一聲冷哼從身側響起傻寂,我...
    開封第一講書人閱讀 37,761評論 0 268
  • 序言:老撾萬榮一對情侶失蹤息尺,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后疾掰,有當地人在樹林里發(fā)現(xiàn)了一具尸體搂誉,經...
    沈念sama閱讀 44,217評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,545評論 2 327
  • 正文 我和宋清朗相戀三年静檬,在試婚紗的時候發(fā)現(xiàn)自己被綠了炭懊。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,694評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡拂檩,死狀恐怖侮腹,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情稻励,我是刑警寧澤父阻,帶...
    沈念sama閱讀 34,351評論 4 332
  • 正文 年R本政府宣布,位于F島的核電站望抽,受9級特大地震影響加矛,放射性物質發(fā)生泄漏。R本人自食惡果不足惜煤篙,卻給世界環(huán)境...
    茶點故事閱讀 39,988評論 3 315
  • 文/蒙蒙 一斟览、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧辑奈,春花似錦苛茂、人聲如沸已烤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,778評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽草戈。三九已至,卻和暖如春侍瑟,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背丙猬。 一陣腳步聲響...
    開封第一講書人閱讀 32,007評論 1 266
  • 我被黑心中介騙來泰國打工涨颜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人茧球。 一個月前我還...
    沈念sama閱讀 46,427評論 2 360
  • 正文 我出身青樓庭瑰,卻偏偏與公主長得像,于是被迫代替她去往敵國和親抢埋。 傳聞我的和親對象是個殘疾皇子弹灭,可洞房花燭夜當晚...
    茶點故事閱讀 43,580評論 2 349

推薦閱讀更多精彩內容

  • 2016年國慶假期終于把此書過完穷吮,整理筆記和體會于此。 關于書名 書名源于俄羅斯的演員斯坦尼斯拉夫斯基創(chuàng)作的《演員...
    李劍飛的簡書閱讀 7,224評論 2 65
  • 一饥努、溫故而知新 1. 內存不夠怎么辦 內存簡單分配策略的問題地址空間不隔離內存使用效率低程序運行的地址不確定 關于...
    SeanCST閱讀 7,784評論 0 27
  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理捡鱼,服務發(fā)現(xiàn),斷路器酷愧,智...
    卡卡羅2017閱讀 134,633評論 18 139
  • 生活是個太過沉重的話題驾诈,說不清,道不明溶浴,但依然要拖著身體前行乍迄。也許你讀出了消極的氣息,但生活就是不斷地挑戰(zhàn)自己的心...
    心存善念王李軍閱讀 594評論 4 18
  • 皺紙撫未平士败,筆沁舊書音闯两。 三言兩句半,字字盡誅心拱烁。 前有孟氏女生蚁,后有卓文君。 如是秦河柳戏自,西湖小妹墳邦投。 宋詞本十色...
    寒菊閱讀 452評論 0 2