概述
假设有这样一种应用场景,有一个/lib/libfoo.so动态库,有两个或多个厂家各自实现了自己的版本,每个版本都不是尽善尽美,分别有自己的优势和缺点。可能app1使用v1版本的库比较合适、app2使用v2版本的库不会出bug等等。
在不能修改应用程序和动态库的前提下,本文提供了一种可以简单有效在运行时切换动态库的方法。
示例
v1版本库代码,v1/foo.c:#include
void foo1(void)
{
printf("This is v1 foo1n");
}
void foo2(void)
{
printf("This is v1 foo2n");
}
v2版本库代码,v2/foo.c:#include
void foo1(void)
{
printf("This is v2 foo1n");
}
void foo2(void)
{
printf("This is v2 foo2n");
}
两个版本都实现了foo1、foo2函数,分别打印自己的版本号和函数名。
用于切换的库代码,./gate.c:#include
#include
#include
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
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.out
v1:
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
v2:
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 foo2
root@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"段中,动态库加载时自动运行。
看来哪里的程序员都差不多,脾气不怎么好,直接代码里吐槽: