解決動態庫的符號衝突

天存資訊 發表於 2021-05-26

圖片

一次debug遇到的疑惑

某天發現一個程式有點問題。祭上print大法,在關鍵的 lib_func() 函式裡新增 print 除錯資訊,重新編譯執行。

期望 print 出的資訊一點都沒有,但是程式確確實實又執行過了 libfunc() ,因為除了新增的除錯 print 沒有執行,libfunc() 該有的功能都執行了。這真是奇怪了。

程式不會騙人。執行的 libfunc() 肯定不是我們修改後的那個 libfunc() ,一定是別的地方有原版的 lib_func() 被執行了。一番調查,果然如此。為了便於說明,把程式和現象簡化說明如下:

程式包含如下程式碼檔案——

main.c # 主程式
plugin.c # 外掛程式
lib.c lib.h # 一個庫

Makefile如下:

all:main plugin
main:
    cc -o main main.c lib.c -ldl -rdynamic
plugin:
    cc -shared -fPIC -o plugin.so plugin.c lib.c

編譯後,生成可執行程式main, 和動態庫檔案 plugin.so。其中主程式執行的時候,會動態載入外掛 plugin.so (呼叫了 lib.c 裡的程式)並執行。

懷疑出問題的地方在lib.c裡。修改後的lib.c內容如下,新增了debug字樣。

void lib_func() {   
// fprintf(stderr, "%s()\n", __func__);
    fprintf(stderr, "debug:%s()\n", __func__);
}

因為主程式沒問題,就只重新編譯了 plugin.so ,重新執行。

期望得到的print的結果是 debug:libfunc() ,實際得到卻是 libfunc()

看起來,一個程式裡的確有兩份 lib_func() 程式碼。從Makefile可以看出,一份在main程式裡,一份在plugin.so裡。實際執行的是main裡的那份。

事情忽然就有意思了:如果一個程式裡包含多個相同的函式,實際執行的是哪一個?

TIPS:可以簡單的使用linux的命令 nm <程式檔案> 檢視程式裡有哪些函式

動態庫和符號表

儘管程式各不相同,但總有些功能很常見。每個程式都為他們寫一遍程式碼很不划算,於是獨立出來成了庫,在多個程式之間共享。一個庫也可以使用別的庫。有兩種共享的辦法:靜態的,動態的。

在編譯時,把庫的程式碼複製一份合併到可執行檔案裡的,是靜態庫。

在執行時,把庫的程式碼載入一份到記憶體裡的,是動態庫。

動態庫更節省資源,不用被複制很多次,更新也方便。

負責連結的東西,叫做連結器(linker),負責載入的叫做載入器(loader)。

然而計算機是根據地址來執行的,每條指令執行前都要先確定地址。動態庫載入之前,誰都不知道它會被載入到哪裡,也就不知道動態庫裡的指令的地址,只能通過符號(名稱)來記錄它提供給別人用的函式列表(匯出表),以及它期望別人提供給他的函式列表(匯入表)。庫被載入後,就獲得了地址。程式執行前,需要先解析符號表,確定每個符號的實際地址。

我們開頭的例子,兩個相同名字的 libfunc() ,一個在main程式裡,一個在plugin.so裡,main先載入,plugin.so使用的 libfunc() 就被解析到了main的 libfunc() 。執行了老的 libfunc() ,而不是我們修改過的帶有debug版本。

TIPS:對程式連結和載入有興趣的同學可以看看 Linkers and Loaders 這本書,非常詳細。

和符號有關的編譯器選項和環境變數選項

如果條件允許,儘量不要在同一個程式中出現兩份程式碼,出現相同符號的情況,造成衝突。

如果出現了符號衝突一定要解決:如本例中,假設 main 不可變,已經包含了 lib 的程式碼。plugin.so 可通過 gcc 的 -Wl,-Bsymbolic選項告訴載入器優先使用自己的符號,而不優先用全域性的符號。該選項可以解決符號衝突。

TIPS: 如果想觀察載入器的工作,可以使用環境變數 LD_DEBUG=all ./main 來執行程式,會獲得詳細的解析過程。manpage的 ld.so(8) 有更多詳細的說明。

最後附上示例用的程式碼:

➜  sample cat lib.h
#ifndef UNTITLED_LIB_H
#define UNTITLED_LIB_H

void lib_func();

#endif //UNTITLED_LIB_H
➜  sample cat lib.c
#include <stdio.h>
#include "lib.h"

void lib_func() {
    //fprintf(stderr, "%s()\n", __func__); 
    fprintf(stderr, "debug:%s()\n", __func__);
}
➜  sample cat main.c  
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
#include "lib.h"

int main() {
    void *handle = dlopen("./plugin.so", RTLD_NOW);
    void (*plugin_func)() = (void (*)()) dlsym(handle, "plugin");
    plugin_func();
    return 0;
}
➜  sample cat plugin.c 
#include "lib.h"

void plugin() {
    lib_func();
}
➜  sample cat Makefile 
all:main plugin

main:
    cc -o main main.c lib.c -ldl -rdynamic
plugin:plugin.c lib.c
    #cc -shared -fPIC -o plugin.so plugin.c lib.c -Wl,-Bsymbolic
    cc -shared -fPIC -o plugin.so plugin.c lib.c

clean:
    rm -f main plugin.so
➜  sample 

(陳國 | 天存資訊)