相比在编译期间显式链接,运行前隐式加载的方式,运行时显式链接并加载的方式显然更加灵活。
这种方式可以控制程序在需要时加载指定模块,甚至可以在不需要时卸载,从而减少程序启动时间和内存消耗,以及实现热更新这种功能。
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"中查找关联的符号,这点类似于dlsyminfo
用来存放关联的共享对象(或可执行文件)以及符号信息,具体结构如下: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;
}
以下例子用来印证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;
}
以下例子是一个基于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);
}
装载顺序可以通过ldd命令来确定:
这种使用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;
}