一次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
(陳國 | 天存信息)