動態連結器(the dynamic linker)
/ ld-dot-so, said ell-dee-dot-so /
一個使用共享函式庫的程式到達磁碟時是不完整的:它知道自己需要像 printf() 這樣的函式但不含它們,因為它們住在 libc.so 裡,那是系統上每個程式共享的。每次程式啟動時,得有人去找到那些函式庫、把它們載入記憶體、並接好缺少的位址。那個人就是動態連結器——一個小程式(在 Linux 上是 ld.so,更精確說是 ld-linux-x86-64.so.2),核心會在你的程式碼之前先執行它,以在載入時完成連結工作。
它依序做這些事。當你 exec 一個動態連結的程式時,核心注意到程式指名了一個直譯器(記在特殊的 PT_INTERP 區段裡),於是先載入「那個」——動態連結器——並把真正的程式交給它。動態連結器讀程式的 .dynamic 節區,找到它所需函式庫的清單(DT_NEEDED 標籤),在磁碟上搜尋每一個、把它映射進記憶體,然後對那些函式庫所需的函式庫遞迴地做同樣的事。一切映射完後,它執行載入期重定位:把每個 GOT 槽填入它所指符號的真正位址,跨所有已載入物件解析名字。直到這一切之後,它才跳到程式的進入點,你的程式碼開始執行。它也能稍後在執行期透過 dlopen() 被呼叫,以按需載入外掛。
它重要,是因為它是使共享函式庫實用的那一塊:磁碟與記憶體裡各一份 libc,被數百個行程共享,在啟動時被縫進每一個裡。它也是載入期行為被控制之處——像 LD_LIBRARY_PATH 與 LD_PRELOAD 這樣的環境變數由動態連結器讀取,你也可以用 LD_DEBUG 要它印出它的決定。一個常見誤解是「連結器」是同一個東西;事實上有兩個不同的程式:靜態連結器(ld)在建構時執行一次以產生二進位檔,動態連結器(ld.so)在二進位檔每次啟動時執行以完成工作。
$ readelf -l app | grep interpreter [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] $ ldd app # 問動態連結器它會載入哪些函式庫 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f...)
核心先載入直譯器(ld.so);它接著找到並映射每個所需函式庫、解析符號,然後你的程式碼才執行。
有兩個不同的連結器:靜態連結器(ld)在建構時執行一次;動態連結器(ld.so)在每次啟動時執行。一個完全靜態的二進位檔沒有 PT_INTERP,根本不需要動態連結器。