Linux下一種運行時切換動態庫的方法

概述

假設有這樣一種應用場景,有一個/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.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"段中,動態庫加載時自動運行。

看來哪裏的程序員都差不多,脾氣不怎麼好,直接代碼裏吐槽:
吐槽


公衆號二維碼

發佈了6 篇原創文章 · 獲贊 10 · 訪問量 3011
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章