概述
假設有這樣一種應用場景,有一個/lib/libfoo.so
動態庫,有兩個或多個廠家各自實現了自己的版本,每個版本都不是盡善盡美,分別有自己的優勢和缺點。可能app1使用v1版本的庫比較合適、app2使用v2版本的庫不會出bug等等。
在不能修改應用程序和動態庫的前提下,本文提供了一種可以簡單有效在運行時切換動態庫的方法。
示例
- v1版本庫代碼,v1/foo.c:
#include <stdio.h>
void foo1(void)
{
printf("This is v1 foo1\n");
}
void foo2(void)
{
printf("This is v1 foo2\n");
}
- v2版本庫代碼,v2/foo.c:
#include <stdio.h>
void foo1(void)
{
printf("This is v2 foo1\n");
}
void foo2(void)
{
printf("This is v2 foo2\n");
}
兩個版本都實現了foo1、foo2函數,分別打印自己的版本號和函數名。
- 用於切換的庫代碼,./gate.c:
#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
static void gate(int argc, char *argv[], char *envp[])
{
if (strstr(argv[0], "v1")) {
dlopen("v1/libfoo.so", RTLD_GLOBAL | RTLD_NOW);
} else {
dlopen("v2/libfoo.so", RTLD_GLOBAL | RTLD_NOW);
}
}
static void *array[] __attribute__((section(".init_array"))) = {&gate};
gate.c實現了切換代碼,切換條件爲當app名稱包含v1字符串時,切換到v1/libfoo.so。當app名稱包含v2字符串時,切換到v2/libfoo.so。
實際應用時可以根據現實情況進行判斷切換。
- 測試app代碼,./test.c:
#include <dlfcn.h>
extern void foo1(void);
extern void foo2(void);
void main(void)
{
void (*func)();
void *handle;
foo1();
handle = dlopen("libfoo.so", RTLD_GLOBAL);
func = dlsym(handle, "foo2");
func();
}
分別測試了直接調用和通過dlsym調用的情況。
- Makefile:
all:
gcc -shared v1/foo.c -o v1/libfoo.so
gcc -shared v2/foo.c -o v2/libfoo.so
gcc -shared gate.c -o libfoo.so -ldl
gcc test.c -o v1.out -lfoo -ldl -L./v1
ln -sf v1.out v2.out
v2.out是指向v1.out的軟鏈接,只有文件名不一樣。
- 目錄結構:
root@debian:~# ll *
-rw-r–r-- 1 root root 335 Dec 25 13:07 gate.c
-rwxr-xr-x 1 root root 4772 Dec 25 12:59 libfoo.so
-rw-r–r-- 1 root root 181 Dec 25 13:20 Makefile
-rw-r–r-- 1 root root 216 Dec 25 13:08 test.c
-rwxr-xr-x 1 root root 5496 Dec 25 12:59 v1.out
lrwxrwxrwx 1 root root 6 Dec 25 12:59 v2.out -> v1.outv1:
total 12
-rw-r–r-- 1 root root 121 Dec 25 12:54 foo.c
-rwxr-xr-x 1 root root 4688 Dec 25 12:59 libfoo.sov2:
total 12
-rw-r–r-- 1 root root 121 Dec 25 12:54 foo.c
-rwxr-xr-x 1 root root 4688 Dec 25 12:59 libfoo.so
- 運行結果
root@debian:~# ldd v1.out
linux-gate.so.1 (0xb77ac000)
libfoo.so => ./libfoo.so (0xb77a5000)
libdl.so.2 => /lib/i386-linux-gnu/libdl.so.2 (0xb7796000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7621000)
/lib/ld-linux.so.2 (0x8005e000)root@debian:~# ldd v2.out
linux-gate.so.1 (0xb77bc000)
libfoo.so => ./libfoo.so (0xb77b5000)
libdl.so.2 => /lib/i386-linux-gnu/libdl.so.2 (0xb77a6000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7631000)
/lib/ld-linux.so.2 (0x8005b000)root@debian:~# ./v1.out
This is v1 foo1
This is v1 foo2root@debian:~# ./v2.out
This is v2 foo1
This is v2 foo2
原理分析
動態庫加載時,不管是dlopen主動加載,還是被應用程序或其它動態庫被動關聯加載,都會執行動態庫的初始化函數。該動作給了一個運行時動態切換運行庫的契機。
ElfW(Dyn) *preinit_array = main_map->l_info[DT_PREINIT_ARRAY];
ElfW(Dyn) *preinit_array_size = main_map->l_info[DT_PREINIT_ARRAYSZ];
addrs = (ElfW(Addr) *) (preinit_array->d_un.d_ptr + main_map->l_addr);
for (cnt = 0; cnt < i; ++cnt)
((init_t) addrs[cnt]) (argc, argv, env);
if (l->l_info[DT_INIT] != NULL)
DL_CALL_DT_INIT(l, l->l_addr + l->l_info[DT_INIT]->d_un.d_ptr, argc, argv, env);
ElfW(Dyn) *init_array = l->l_info[DT_INIT_ARRAY];
addrs = (ElfW(Addr) *) (init_array->d_un.d_ptr + l->l_addr);
for (j = 0; j < jm; ++j)
((init_t) addrs[j]) (argc, argv, env);
加載時分別執行PREINIT_ARRAY函數數組、INIT函數、INIT_ARRAY函數數組。gate.c將初始化函數放在".init_array"段中,動態庫加載時自動運行。
看來哪裏的程序員都差不多,脾氣不怎麼好,直接代碼裏吐槽: