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