Linux 執行時尋找 symbol 的流程以及 shared library 相關知識
針對 Linux 和 ELF,這篇文章回答以下的問題:
- 為何需要 shared library?
- 如何解決執行時找不到某個 symbol / library?
- 如何在執行時替換特定的 symbol?
- 有多個重覆的 symbol 時,怎麼確保執行時用到期望的 symbol?
symbol 可以是變數或函式。
static library 和 shared library
static library 只是將一堆 object file 包在一個檔案,然後在產生 executable 時,從中取出用到的部份,加入 executable。由於 symbol 是直接加入 executable 裡,執行時沒什麼需要擔心的問題。
相較於 static library,shared library 有以下好處:
- 節省硬碟空間 (避免多個 executable 有重覆的程式和資料)。
- 節省 OS 記憶體 (被多個 process 共用時,程式碼只占用一份空間)。
- 方便替換 library 而不用重編程式 (有時甚至是無法重編程式)。
- 開發時節省 linking 時間。
- 設定 symbol visibility 避免使用模組的 private API,確保模組的封裝效果。symbol visibility 的說明見《Linux 編譯 shared library 的方法和注意事項》。
- 符合 LGPL (商業版程式用到 LGPL library 但不想 open source 自家程式時,必須用 shared library 以符合 LGPL)。
缺點則是行為變得複雜許多。
另外,若要最佳的執行效率以及安全性 (避免被替換函式),將全部 symbol 編入一個執行檔是最安全的。以 Chrome 為例,Chrome 正式版會盡可能編成一個執行檔,稱為 “static build”; 平時開發則用 “component build” (各個模組編成各自的 shared library),節省開發時間。在 static build 的情況,Link Chrome 執行檔要8G 以上的記憶體花費數分鐘時間。反觀 component build 的情況,多數 components 只需幾秒完成 link,只有 Blink 需要較久的時間。
基本觀念
對有使用 shared library 的 executable 來說,檔案內含有一串未定義的 symbol 以及一串使用到的 shared library。要注意的是,ELF 沒有指定每個 symbol 要去那個 shared library 找,兩個列表獨立使用。這個設計提供執行時替換 symbol 實作的彈性。
可以用 nm -Du
列出未定義需要外部提供的 symbol:
$ nm -Du /bin/ls
U abort
U __assert_fail
U bindtextdomain
U calloc
U clock_gettime
U close
U closedir
U __ctype_b_loc
U __ctype_get_mb_cur_max
U __ctype_tolower_loc
...
可以用 ldd 列出需要的 shared library:
或是用 objdump:
$ objdump -p /bin/ls
...
Dynamic Section:
NEEDED libselinux.so.1
NEEDED libc.so.6
...
ldd 是個 shell script,會遞迴列出全部用到的 shared library,比較好用。
另外,對於執行中的 process,可以查看 /proc/PID/maps
得知它目前用到的 shared library。man proc 可得知 /proc/PID/maps
更多資訊,像是如何得知 PID 目前自己使用到的 memory 數量 (private memory)。
執行期尋找 shared library 的流程
執行程式的時候,dynamic (runtime) linker (ld.so) 會依以下順序尋找 ( man ld.so 有詳細說明) executable 內列的 shared library:
- 若 shared library 名稱內有 “/”,表示它是路徑,直接用這個路徑找。
- 若 executable 內有定義
DT_RPATH
沒定義DT_RUNPATH
,從DT_RPATH
列的目錄裡找。 - 從
LD_LIBRARY_PATH
列的目錄裡找。 - 從
DT_RUNPATH
列的目錄裡找。 - 從
ldconfig
產生的 cache 內找 (/etc/ld.so.cache
)。 - 從 OS 的預設位置找: 先找
/lib
再找/usr/lib
。
用 LD_DEBUG=libs
可以看找的過程。以我自己編 Chromium 的 component build 為例:
可以看出 libpthread
最後從 /etc/ld.so.cache
找到,而 libc++
則是透過 rpath
先找到 Chrome 自己帶的版本,就沒有繼續找 OS 帶的版本。
執行期尋找 symbol 的流程
有了 shared library 的路徑列表後,每看到一個 symbol,先從 executable 找,找不到再照 shared library 的順序找,找到第一個符合的 symbol 就用。shared library 的順序是連結 executable 時決定的,和 ldd 列的順序一致。注意,executable 可以替換掉 shared library 內使用的 global symbol,甚至是 shared library 自己定義的 global symbol (例如標準函式庫內使用自己定義的 malloc)。編譯 shared library 時可下參數改變此行為,詳情可參考《Linux 編譯 shared library 的方法和注意事項》。
以 Chromium 66 的 component build 為例,ldd 會看到兩個 fontconfig:
libfontconfig.so
=> /path/to/chromium/out/Debug/./libfontconfig.solibfontconfig.so.1
=> /usr/lib/x86_64-linux-gnu/libfontconfig.so.1
第一個指到 Chromium 自己帶的版本; 第二個指到 OS 安裝的版本。由於 libfontconfig.so 在 libfontconfig.so.1 前面,所以用到 fontconfig 的各個函式時,都會先從 libfontconfig.so 找到。這個結果符合 Chromium team 的預期,可能因此沒人發現 component build 有兩份 libfontconfig 吧。
這裡有個簡單的例子: libfoo 和 libbar 都有定義 xyz()。執行 link 的時候,參數內 shared library 的順序 (左邊比右邊優先) 決定用到誰的版本。
可以用 LD_DEBUG=symbols
看找 symbol 的流程,若 link 時 libfoo 優先於 libbar,執行結果如下:
由於 libfoo.so 和 libbar.so 沒有在 /etc/ld.so.cache
內也沒有在 OS 的標準位置內,所以需要用 LD_LIBRARY_PATH
指定它們的位置。
由上面的執行結果可知,找的順序是 ./prog > ./libfoo.so > ./libbar.so,所以在 ./libfoo.so 內找到 xyz 後就不會再往後找了。
用 LD_PRELOAD 替換 symbol 實作
如前所述,可以透過調整 shared library 在命令列的順序決定先使用誰的實作。若不方便調順序或是不想重新 link 的話,可以用 LD_PRELOAD
。
若有定義環境變數 LD_PRELOAD
或 /etc/ld.so.preload
內有列 shared library 位置時,ld.so 會先從這裡找 symbol (詳細格式見 man ld.so)。
下面是替換標準函式庫 putchar
的例子:
$ cat mylib.c
#include <stdio.h>int putchar(int c) {
printf("call putchar() with %d\n", c);
return c;
}
$ cat main.c
#include <stdio.h>int main(void) {
putchar('X');
putchar('\n');
return 0;
}
$ gcc -Wall -fPIC -shared -o libmylib.so mylib.c
$ gcc -o main main.c
$ ./main
X
$ LD_PRELOAD=./libmylib.so ./main
call putchar() with 88
call putchar() with 10
這裡先著重在 LD_PRELOAD
的效果, gcc 參數的意思可參考《Linux 編譯 shared library 的方法和注意事項》。
若想實作 wrapper (例如追踪 malloc
/free
使用情況),需要呼叫原本的函式,要用到 GNU 的延伸功能 RTLD_NEXT
,表示載入「原本規則找到的 symbol 」的下一個 symbol。
下面是替換 malloc
的例子:
$ cat mem.c
// RLTD_NEXT is only supported in _GNU_SOURCE.
#define _GNU_SOURCE#include <stdio.h>
#include <dlfcn.h>void *malloc(size_t size) {
static int s_calling = 0;
void* (*m)(size_t);// 「標準」取得 function pointer 的寫法. 見 TLPI 42.1.2 p863 的說明
*(void**)(&m) = dlsym(RTLD_NEXT, "malloc");
if (s_calling) {
// Avoid recursion.
return m(size);
} else {
s_calling = 1;
printf("malloc size=%zu\n", size);
s_calling = 0;
return m(size);
}
}
$ cat main2.c
#include <stdio.h>
#include <stdlib.h>int main(void) {
char* s = malloc(10);
return 0;
}
$ gcc -Wall -fPIC -shared -o libmem.so mem.c -ldl
$ gcc -g -o main2 main2.c
$ LD_PRELOAD=./libmem.so ./main2
malloc size=10
dlsym() 用來從 shared library 動態載入 symbol,更多的說明見 The Linux Programming Interface (TLPI) 42.1。
Recap: 如何解決執行期找不到 shared library?
先用 LD_LIBRARY_PATH
指定 shared library 所在的位置,確保可以正常執行。
正解則是在用到目標 shared library X 的 executable / shared library 裡加入 rpath
指定 X 的位置。或是複製 X 到 ldconfig 掃瞄的位置。詳情可參考《Linux 編譯 shared library 的方法和注意事項》。
Recap:如何解決執行期找不到 symbol?
- 先了解 symbol 定義在那個 library 裡 (這裡假設在 X 裡),然後用
LD_DEBUG=libs
看是否有載入目標 X。沒有的話,可以先用LD_LIBRARY_PATH
指定 X 的位置,藉此確認「有載入目標 library 的情況,可以解決問題。」 - 載入 X 沒解決問題的話,用
nm -D
檢查 symbol 是否有在 library 內。沒有的話要先解決這個問題。 - 確認載入 X 後可解決問題,可以在用到 symbol 的 executable / shared library 內加入
rpath
指定 X 的位置。或是複製 X 到 ldconfig 掃瞄的位置。詳情可參考《Linux 編譯 shared library 的方法和注意事項》。
Recap: 如何確定執行期用對 symbol?
用 LD_DEBUG_OUTPUT=/path/to/log LD_DEBUG=symbols PROG
,查看結果。關於 LD_DEBUG 的其它用法,可以看 man ld.so 還有 LD_DEBUG=help PROG
。
參考資料
- The Linux Programming Interface: ch41 Fundamentals of Shared Libraries
- The Linux Programming Interface: ch42 Advanced Features of Shared Libraries
- man ld.so