顯式運行時加載

相比在編譯期間顯式鏈接,運行前隱式加載的方式,運行時顯式鏈接並加載的方式顯然更加靈活。
這種方式可以控制程序在需要時加載指定模塊,甚至可以在不需要時卸載,從而減少程序啓動時間和內存消耗,以及實現熱更新這種功能。

linux上提供了以下4個api來支持共享對象的顯式運行時訪問

  • void *dlopen(const char *filename, int flags) 加載一個共享對象, 同一個共享對象不會重複加載(只會遞增一下計數器)
    • filename 要加載的共享對象
      如果傳入NULL則返回的句柄不但包括了主程序本身,還包括了程序啓動時自動加載的共享對象,以及使用RTLD_GLOBAL方式的dlopen打開的共享對象
    • flags 指定該共享對象中符號的解析方式,有2個標誌必選其一:
      RTLD_LAZY 對該共享對象中的函數符號使用延遲綁定策略,而變量符號總是在該共享對象加載時就立即解析
      RTLD_NOW 加載時就解析完所有符號
      另有以下幾個可選標誌:
      RTLD_GLOBAL 指定該共享對象中的符號對在其後加載的共享對象可見(即便該共享對象使用了延遲綁定策略,這種向後可見性仍舊有效)
      RTLD_LOCAL 指定該共享對象中的符號對在其後加載的共享對象不可見(這也是RTLD_GLOBAL和RTLD_LOCAL都不指定時的缺省特性)
  • int dlclose(void *handle) 遞減目標共享對象的引用計數,如果遞減後爲0則卸載該共享對象
  • void *dlsym(void *handle, const char *symbol) 從目標句柄包含的共享對象(或可執行文件)的".dynsym"中查找目標符號,然後返回對應的運行時地址
    • handle 除了可以是dlopen返回的句柄外,還可以是以下2個特殊的僞句柄:
      RTLD_DEFAULT 從全局作用域中按裝載順序查找目標符號,返回第一個, 作用類似於傳入一個特殊dlopen句柄(filename爲NULL),顯然使用這個僞句柄的好處是可以省去dlopen和dlclose操作
      RTLD_NEXT 從全局作用域中按裝載順序查找目標符號,返回第二個,最常見的應用場景是封裝標準庫中的malloc和free以實現內存使用跟蹤
  • int dladdr(void *addr, Dl_info *info) 獲取目標地址關聯的共享對象(或可執行文件)以及符號的運行時信息
    如果找不到目標地址關聯的共享對象(或可執行文件)則返回0,否則返回一個非0值;如果能找到關聯的共享對象但找不到關聯的符號信息則Dl_info中的dli_sname和dli_saddr字段的值爲NULL
    本接口只會在可執行文件和已經加載的共享對象的".dynsym"中查找關聯的符號,這點類似於dlsym
    • info 用來存放關聯的共享對象(或可執行文件)以及符號信息,具體結構如下:
      typedef struct {
          const char *dli_fname;  // 包含目標地址的共享對象(或可執行文件)路徑名
          void *dli_fbase;        // 該共享對象加載到進程虛擬地址空間中的基址
          const char *dli_sname;  // 包含目標地址的符號名
          void *dli_saddr;        /* 該符號名運行時的準確起始地址, 該地址不一定等於目標地址,
                                     比如函數符號名涵蓋了該函數整個函數體指令區域的地址,
                                     而函數名的準確起始地址指的是第一條指令的地址 */
      }Dl_info 
      

以下例子用來印證RTLD_GLOBAL和RTLD_DEFAULT的作用:

// hello.c
#include <stdio.h>

void say_hello(void)
{
    printf("hello!\n");
}
// say.c
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

typedef void(*say_t)(void);

void say(void)
{
    say_t hello = dlsym(RTLD_DEFAULT, "say_hello");   // 此刻的全局作用域中包含了事先使用RTLD_GLOBAL方式的dlopen打開的libhello.so
    if (!hello) {
        printf("%s\n", dlerror());
        exit(EXIT_FAILURE);
    }
    hello();
}
// dlmain.c
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

typedef void(*say_t)(void);

int main(int argc,  char *argv[]) {
    void *handle = NULL;
#ifdef GLOBAL
    handle = dlopen("./libhello.so", RTLD_LAZY| RTLD_GLOBAL);   // RTLD_GLOBAL使得libhello.so動態符號表中的符號對後續加載的共享對象可見
#elif LOCAL
    handle = dlopen("./libhello.so", RTLD_LAZY| RTLD_LOCAL);    // RTLD_LOCAL使得libhello.so中的符號對後續加載的共享對象不可見
#endif
    if (!handle) {
        fprintf(stderr, "%s\n", dlerror());
        exit(EXIT_FAILURE);
    }

    handle = dlopen("./libsay.so", RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "%s\n", dlerror());
        exit(EXIT_FAILURE);
    }

    say_t say = dlsym(handle, "say");
    if (!say) {
        fprintf(stderr, "%s\n", dlerror());
        exit(EXIT_FAILURE);
    }
    say();

    return 0;
}

result

以下例子用來印證dlsym只能從".dynsym"中查找符號:

// dlmain.c
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

// 非動態庫中的全局函數bye默認不會記錄在動態符號表,所以無法被dlsym找到
void bye(void)
{
    printf("bye\n");
}

int main(int argc,  char *argv[]) {
    typedef void(*say_t)(void);
    say_t say = dlsym(RTLD_DEFAULT, "bye");
    if (!say) {
        fprintf(stderr, "%s\n", dlerror());
        exit(EXIT_FAILURE);
    }
    say();
    return 0;
}

result

以下例子是一個基於RTLD_NEXT實現的malloc和free封裝模塊

// main.c
#include <stdio.h>
#include <stdlib.h>

int main(int argc,  char *argv[]) {
    /* 默認取第一個找到的malloc符號,而按照裝載順序,第1順序的malloc符號位於自定義的libmem.so庫中
     * 從而實現了對malloc的封裝,free同理
     */
    void *p = malloc(8);
    if (!p) {
        perror("malloc");
        exit(EXIT_FAILURE);
    }

    free(p);
}
// mem.c
#include <dlfcn.h>
#include <stdlib.h>
#include <stdio.h>

void *malloc(size_t len)
{
    fprintf(stderr, "malloc %lu bytes\n", len);

    typedef void *(*malloc_t)(size_t len);
    /* 按照裝載順序,首先找到的malloc符號位於當前動態庫中,其次才位於標準庫libc.so.6中
     * 這裏取第2順序,即標準庫中的malloc, free同理
     */
    malloc_t m = dlsym(RTLD_NEXT, "malloc");    
    if (!m) {
        return NULL;
    }

    return m(len);
}

void free(void *p)
{
    fprintf(stderr, "free memory at %p\n", p);

    typedef void (*free_t)(void *p);
    free_t f = dlsym(RTLD_NEXT, "free");
    if (!f) {
        return;
    }

    f(p);
}

result

裝載順序可以通過ldd命令來確定:
order

這種使用RTLD_NEXT封裝標準庫malloc和free接口的方式極其容易導致程序crash,所以生產環境慎用(建議改用tcmalloc等)!


以下例子用來印證dladdr只能從".dynsym"中查找符號:

// dladdr.c
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

int g_a = 3;

int main(int argc,  char *argv[]) {
    Dl_info info;
    int res = dladdr(&g_a, &info);
    if (!res) {
        fprintf(stderr, "%s\n", dlerror());
        exit(EXIT_FAILURE);
    }

    printf("share object: %s\n", info.dli_fname);
    if (!info.dli_sname) {
        fprintf(stderr, "fail to find symbol\n");
        exit(EXIT_FAILURE);
    }
    printf("symbol name: %s\n", info.dli_sname);

    return 0;
}

result

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