显式运行时加载

相比在编译期间显式链接,运行前隐式加载的方式,运行时显式链接并加载的方式显然更加灵活。
这种方式可以控制程序在需要时加载指定模块,甚至可以在不需要时卸载,从而减少程序启动时间和内存消耗,以及实现热更新这种功能。

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值