Explicit Runtime Linking

explicit runtime linking 讓程式可以在執行過程中控制 load 及 unload 模組。這種方式讓載入模組變得更靈活,可以在程式需要時才載入某個模組,載入什麼模組也可以依執行狀況決定。因為不需要在一開始載入所有模組,所以可以減少啟動時間跟記憶體用量。另外,因為可以在執行期才載入,也就能在不關閉程式的情況下新增、刪除及更新模組。

在程式碼裡指定了什麼時候要 load 哪個模組,對程式來說它就「知道」要有載入模組的動作。如果是以 dynamic linker 來 load 跟 link,模組跟 library 對程式是透明的,它不知道它用了什麼。兩者的差別即在手動或自動 load shared library,以及load 的時機。

另一方面,如果需要知道一個程式依賴哪些模組,例如移植時要將所有相關模組移植到其他平台以確保所有功能正常。這時以 explicit runtime linking 載入的模組可能增加檢查 dependency 的難度,因為無法用 ldd 得知依賴的模組,可能要等到用到某個功能時才當掉或無法使用。當然這問題並不是太大的缺點,可以透過適當管理 dependency library 以及以一致的方式撰寫 explicit linking 程式碼來避免。

linux 以四個 function 來達到 explicit runtime linking,分別是 dlopen()dlsym()dlerror() 以及 dlclose(),它們宣告在 <dlfcn.h>,link 時須 -ldl

dlopen()

dlopen() 開啟一個 dynamic library,將其 load 到 process 的 adddress space,並完成初始化。

void *dlopen(const char *filename, int flag);

第一個參數 filename 很簡單,就是 dynamic library 的路徑。如果是絕對路徑就能直接打開 dynamic library,如果是相對路徑則會依照某個順序尋找 library,詳細的順序可參考 man dlopen

flag 必須指定 RTLD_LAZYRTLD_NOW 之一,RTLD_LAZY 表示 lazy binding,也就是 symbol 在第一次被用到的時候才 resolve。lazy binding 只用於 function,variable 會在 library load 時就 bind 好。RTLD_NOW 當然就是在 load library 的時候就會把所有 symbol 都 resolve。除了 RTLD_LAZYRTLD_NOW 之外,還有 RTLD_GLOBAL 可以跟他們一起使用,表示之後載入的 library 都可以使用這個 library 的 symbol。其他更多 flag 就參考 manual 囉。

dlopen() return 一個 handle,之後的操作要傳這個 handle 給其他 function。如果 load 失敗,會 return NULL。如果要 load 的 library 之前已經 load 過,會 return 跟之前相同的 handle。

如果 load 進來的 library 依賴其他 shared library(link 時指定,非 explicit runtime linking),dlopen() 會自動 load 那些 shared library。

dlsym()

讓我們可以找到 library 的 symbol。

void *dlsym(void *handle, const char *symbol);

handle 是剛剛 dlopen() return 的 handle。

symbol 是要找的 symbol 的名字,如果找不到 symbol 會 return NULL。找到 symbol 會 return 該 symbol 的 address。根據 manual,return 值是 NULL 無法當作找不到 symbol 的判斷,因為 return 值有可能真的是 NULL(不過我想不到這種狀況的例子…),須配合 dlerror() 才能確認是不是有找到 symbol:首先 call dlerror() 清掉前面的 error,接著 call dlsym() 找 symbol,再 call dlerror() 看是否有 error 來判斷是否找到 symbol。

如果多個 library 有相同的 symbol 會用哪個?dlsym() 會以 BFS 找 library 的 dependency tree,直到找到 symbol 或完全找不到為止。也就是說 load 順序會決定 symbol 同名時使用到誰,先 load 會先用。

dlerror()

call dlopen()dlsym()dlclose() 之後可以 call dlerror() 看上次呼叫有沒有成功。如果成功 dlerror() 會 return NULL,反之則 return error 相關資訊。

char *dlerror(void);

dlclose()

用來 unload library。

int dlclose(void *handle);

系統會用 counter 紀錄一個 library load 的次數,也就是 dlopen() 時加一,dlclose() 則減一,當 counter 變成 0 時 library 才會真的被 unload 掉。unload 的順序跟 load 相反,會先執行 .finit section 的程式碼,接著從 symbol table 中移除相關 symbol,取消 process address space 跟 library 的 memory mapping,最後關閉 library 檔案。

成功 return 0,失敗 return 非 0 值。

Example

簡單用個例子實作跟驗證。以下共有 libfoo.solibbar.solibkar.so 三個 library,主程式 main.c 以 explicit runtime linking load libfoo.so,而 libfoo.so 依賴 libbar.so 以及 libkar.so,後兩者有相同名稱的 function。

bar.c
1
2
#include <stdio.h>
void bar() { printf("bar() in bar.c\n"); }
kar.c
1
2
#include <stdio.h>
void bar() { printf("bar() in kar.c\n"); }

compile with

1
2
$ gcc -g -fPIC -shared -o libbar.so bar.c
$ gcc -g -fPIC -shared -o libkar.so kar.c
foo.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int zoo = 1;
const int coo = 5566;

int foo(int a, int b)
{
printf("foo = %x, &zoo = %x, &coo = %x\n", foo, &zoo, &coo);
return a + b;
}

static int koo(int n)
{
return n + coo;
}

compile with gcc -g -fPIC -shared -o libfoo.so foo.c ./libbar.so ./libkar.so

main.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <stdio.h>
#include <dlfcn.h>

int main()
{
void* handle = NULL;

handle = dlopen("./libfoo.so", RTLD_LAZY);

if (handle == NULL)
{
printf("Load library failed. error: %s\n", dlerror());
return -1;
}

int (*fn)(int, int) = NULL;
dlerror(); // clear error condition
fn = dlsym(handle, "foo");
// fn = dlsym(handle, "koo"); // fail

char* error = dlerror();
if (error != NULL)
{
printf("Load symbol failed. error: %s\n", error);
return -1;
}

printf("fn = %x\n", fn);

int ret = fn(3, 5);
printf("ret = %d\n", ret);

// doesn't check error
int* p = NULL;
p = dlsym(handle, "zoo");
printf("p = %x\n", p);

int* c = NULL;
c = dlsym(handle, "coo");
printf("c = %x, *c = %d\n", c, *c);

void (*bar)() = NULL;
bar = dlsym(handle, "bar");
bar();

getchar(); // just pause process

if (dlclose(handle) != 0)
{
printf("Close library fail. error: %s\n", dlerror());
return -1;
}

return 0;
}

compile with gcc -g -o main main.c -ldl

執行結果顯示拿到主程式 libfoo.so 裡 symbol 的 address,以及 load 到 libbar.so 裡的 bar()

1
2
3
4
5
6
7
$ ./main 
fn = ef40f740
foo = ef40f740, &zoo = ef60faf0, &coo = ef40f7a8
ret = 8
p = ef60faf0
c = ef40f7a8, *c = 5566
bar() in bar.c

process 的 address space:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
00400000-00401000 r-xp 00000000 08:01 11409326                           /home/cjw/source/explicit-load/main
00600000-00601000 rw-p 00000000 08:01 11409326 /home/cjw/source/explicit-load/main
02552000-02573000 rw-p 00000000 00:00 0 [heap]
7fe9bee48000-7fe9bee49000 r-xp 00000000 08:01 11409316 /home/cjw/source/explicit-load/libkar.so
7fe9bee49000-7fe9bf048000 ---p 00001000 08:01 11409316 /home/cjw/source/explicit-load/libkar.so
7fe9bf048000-7fe9bf049000 rw-p 00000000 08:01 11409316 /home/cjw/source/explicit-load/libkar.so
7fe9bf049000-7fe9bf04a000 r-xp 00000000 08:01 11409324 /home/cjw/source/explicit-load/libbar.so
7fe9bf04a000-7fe9bf249000 ---p 00001000 08:01 11409324 /home/cjw/source/explicit-load/libbar.so
7fe9bf249000-7fe9bf24a000 rw-p 00000000 08:01 11409324 /home/cjw/source/explicit-load/libbar.so
7fe9bf24a000-7fe9bf24b000 r-xp 00000000 08:01 11409329 /home/cjw/source/explicit-load/libfoo.so
7fe9bf24b000-7fe9bf44a000 ---p 00001000 08:01 11409329 /home/cjw/source/explicit-load/libfoo.so
7fe9bf44a000-7fe9bf44b000 rw-p 00000000 08:01 11409329 /home/cjw/source/explicit-load/libfoo.so
7fe9bf44b000-7fe9bf5ec000 r-xp 00000000 08:01 7864382 /lib/x86_64-linux-gnu/libc-2.19.so
7fe9bf5ec000-7fe9bf7ec000 ---p 001a1000 08:01 7864382 /lib/x86_64-linux-gnu/libc-2.19.so
7fe9bf7ec000-7fe9bf7f0000 r--p 001a1000 08:01 7864382 /lib/x86_64-linux-gnu/libc-2.19.so
7fe9bf7f0000-7fe9bf7f2000 rw-p 001a5000 08:01 7864382 /lib/x86_64-linux-gnu/libc-2.19.so
7fe9bf7f2000-7fe9bf7f6000 rw-p 00000000 00:00 0
7fe9bf7f6000-7fe9bf7f9000 r-xp 00000000 08:01 7864385 /lib/x86_64-linux-gnu/libdl-2.19.so
7fe9bf7f9000-7fe9bf9f8000 ---p 00003000 08:01 7864385 /lib/x86_64-linux-gnu/libdl-2.19.so
7fe9bf9f8000-7fe9bf9f9000 r--p 00002000 08:01 7864385 /lib/x86_64-linux-gnu/libdl-2.19.so
7fe9bf9f9000-7fe9bf9fa000 rw-p 00003000 08:01 7864385 /lib/x86_64-linux-gnu/libdl-2.19.so
7fe9bf9fa000-7fe9bfa1a000 r-xp 00000000 08:01 7864371 /lib/x86_64-linux-gnu/ld-2.19.so
7fe9bfbf9000-7fe9bfbfc000 rw-p 00000000 00:00 0
7fe9bfc16000-7fe9bfc1a000 rw-p 00000000 00:00 0
7fe9bfc1a000-7fe9bfc1b000 r--p 00020000 08:01 7864371 /lib/x86_64-linux-gnu/ld-2.19.so
7fe9bfc1b000-7fe9bfc1c000 rw-p 00021000 08:01 7864371 /lib/x86_64-linux-gnu/ld-2.19.so
7fe9bfc1c000-7fe9bfc1d000 rw-p 00000000 00:00 0
7ffdd05ea000-7ffdd060b000 rw-p 00000000 00:00 0 [stack]
7ffdd0755000-7ffdd0757000 r-xp 00000000 00:00 0 [vdso]
7ffdd0757000-7ffdd0759000 r--p 00000000 00:00 0 [vvar]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

三個 library 都 load 進來啦~

用 gdb 在 _init_fini 設 break point 可以看到在 dlopen()dlclose() 時會分別執行 libfoo.so 及其他 library 的 _init_fini。同樣適用 GCC 提供的 shared library constructor 及 destructor,Ref

可以用 lddlibfoo.so 的 dependency:

1
2
3
4
5
6
$ ldd libfoo.so 
linux-vdso.so.1 (0x00007fffde785000)
./libbar.so (0x00007fdf77158000)
./libkar.so (0x00007fdf76f57000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fdf76bac000)
/lib64/ld-linux-x86-64.so.2 (0x00007fdf7755a000)

如果 compile libfoo.so 時改變 link 的 shared library 順序,變成 gcc -g -fPIC -shared -o libfoo.so foo.c ./libkar.so ./libbar.so,load 到的 bar() 會變成是 libkar.so 的。

1
2
3
4
5
6
7
$ ./main
fn = a259c740
foo = a259c740, &zoo = a279caf0, &coo = a259c7a8
ret = 8
p = a279caf0
c = a259c7a8, *c = 5566
bar() in kar.c

ldd 看到的順序也會變:

1
2
3
4
5
$ ldd libfoo.so 
linux-vdso.so.1 (0x00007fff4db40000)
./libkar.so (0x00007f5b4ddb5000)
./libbar.so (0x00007f5b4dbb4000)
...