提交 fe672d71 编写于 作者: 编程进阶之路's avatar 编程进阶之路

动态加载库 dlopen dlsym dlclose dlerror dladdr

上级 b486cf9f
......@@ -7,4 +7,6 @@
*.o
*.a
*.so*
*.exe
\ No newline at end of file
*.exe
*.out
.idea
\ No newline at end of file
......@@ -70,4 +70,4 @@
:page 736},
:content {:text "[:span]", :image 1694437268292},
:properties {:color "purple"}}],
:extra {:page 747}}
:extra {:page 753}}
/*
动态加载库:dlopen
*/
#include <dlfcn.h>
/*
动态加载库:dlopen dlsym dlerror dlclose dladdr
*/
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char * argv[]) {
void *lib_handle; /* Handle for shared library */
int ( *func_ptr)(int, int); /* Pointer to function with 2 int arguments*/
int result;
const char *err;
if(argc != 3 || strcmp(argv[1], "--help") == 0) {
fprintf(stderr, "%s lib-path func-name\n", argv[0]);
exit(EXIT_FAILURE);
}
/* load the shared library and get a handle for later use */
lib_handle = dlopen(argv[1], RTLD_LAZY);
if (!lib_handle) {
fprintf(stderr, "dlopen: %s\n", dlerror());
exit(EXIT_FAILURE);
}
/* search library for symbol named in argv[2] */
(void) dlerror(); /* clear dlerror()*/
err = dlerror();
func_ptr = dlsym(lib_handle, argv[2]);
if(err) {
fprintf(stderr, "dlsym: %s\n", err);
exit(EXIT_FAILURE);
}
/* if the address returned by dlsym() is no-NULL, try calling it as a function that takes 2 int arguments */
if ( !func_ptr) {
fprintf(stderr, "%s is NULL\n", argv[2]);
exit(EXIT_FAILURE);
}
else {
result = (*func_ptr)(1, 2);
}
printf("call %s, result is %d\n", argv[2], result);
/* dladdr() */
Dl_info info;
dladdr(func_ptr, &info);
printf("info.dli_fname = %s\n", info.dli_fname);
printf("info.dli_fbase = %p\n", info.dli_fbase);
printf("info.dli_sname = %s\n", info.dli_sname);
printf("info.dli_saddr = %p\n", info.dli_saddr);
dlclose(lib_handle);
return 0;
}
\ No newline at end of file
-
-
\ No newline at end of file
-
- [[Daily Log]]
- NOW 开始 [[USP-Unix System Programming]] 学习 >[2023-09-11 - 2023-09-23](#agenda://?start=1694361600000&end=1695481851958)
SCHEDULED: <2023-09-11 Mon .+1d>
DEADLINE: <2023-09-23 Sat>
:LOGBOOK:
CLOCK: [2023-09-11 Mon 23:11:31]
:END:
\ No newline at end of file
......@@ -386,6 +386,7 @@ collapsed:: true
```
- 上面的命令假设在运行时应用程序的共享库位于包含应用程序的可执行文件的目录的子目录 `lib` 中。这样就能向用户提供一个简单的包含应用程序及相关的库的安装包,同时允许用户将这个包安装在任意位置并运行这个应用程序了(即所谓的“turn-key 应用程序”)
- # 在运行时找出共享库
id:: 64ff23e2-fa89-4472-9a85-37bc0033b8c4
collapsed:: true
- 在解析库依赖时,动态链接器首先会检查各个依赖字符串以确定它是否包含斜线(/),因为在链接可执行文件时如果指定了一个显式的库路径名的话就会发生这种情况。如果找到了一个斜线,那么依赖字符串就会被解释成一个路径名(绝对路径名或相对路径名),并且会使用该路径名加载库。否则动态链接器会使用下面的规则来搜索共享库
- 1. 如果可执行文件的 `DT_RPATH` 运行时库路径列表(`rpath`)中包含目录并且不包含 `DT_RUNPATH` 列表,那么就搜索这些目录(按照链接程序时指定的目录顺序)
......
......@@ -6,8 +6,190 @@
- 共享库预加载
- 使用 `LD_DEBUG` 来监控动态链接器的操作
- # 动态加载库
- 关于动态加载库的函数原型在 `<dlfcn.h>` 头文件中声明
- ## 打开共享库:`dlopen()`
collapsed:: true
- `dlopen()` 函数将名为 `libfilename` 的共享库加载进调用进程的虚拟地址空间并增加该库的打开引用计数
- ```c
void *dlopen (const char *__file, int __mode);
```
- [[#red]]==如果 `__file` 包含了一个斜线(`/`),那么 `dlopen()` 会将其解释成一个绝对或相对路径名==,否则动态链接器会使用 ((64ff23e2-fa89-4472-9a85-37bc0033b8c4)) 介绍的规则来搜索共享库
- [[#blue]]==如果 `__file` 指定的共享库依赖于其他共享库,那么 `dlopen()` 会自动加载那些库==。如果有必要的话,这一过程会递归进行。这种被加载进来的库被称为这个库的依赖树
- [[#red]]==`__mode` 参数是一个位掩码,它的取值是 `RTLD_LAZY``RTLD_NOW` 中的一个==,这两个值的含义分别如下
- **`RTLD_LAZY`**: [[#blue]]==**只有当代码被执行的时候才解析库中未定义的函数符号**==。如果需要某个特定符号的代码没有被执行到,那么永远都不会解析该符号。延迟解析 [[$green]]==只适用于函数引用==,对变量的引用会被立即解析
- 指定 `RTLD_LAZY` 标记能够提供与在加载可执行文件的动态依赖列表中的共享库时动态链接器的常规操作对应的行为
- **`RTLD_NOW`**: [[#blue]]==在 `dlopen()` **结束之前立即加载库中所有的未定义符号**==,不管是否需要用到这些符号,这种做法的结果是打开库变得更慢了,但能够立即检测到任何潜在的未定义函数符号错误,而不是在后面某个时刻才检测到这种错误
- [[$green]]==在调试应用程序时这种做法是比较有用的==,因为它能够确保应用程序在碰到未解析的符号时立即发生错误,而不是在执行了很长一段时间之后才发生错误
- [[#green]]==`__mode` 也可以取其他的值,SUSv3 规定了下列几种标记==
- **`RTLD_GLOBAL`** : 这个库及其依赖树中的符号在解析由这个进程加载的其他库中的引用和通过 `dlsym()` 查找时可用
- **`RTLD_LOCAL`** : 与 `RTLD_GLOBAL` 相反。如果不指定任何常量,那么就取这个默认值。它规定在解析后续加载的库中的引用时这个库及其依赖树中的符号不可用
- Linux 还支持几个并没有在 SUSv3 中进行规定的 `__mode` 标记,如下所示
- **`RTLD_NODELETE`**(自 glibc 2.2 起):在 `dlclose()` 调用中不要卸载库,即使其引用计数已经变成 0 了
- 这意味着在后面重新通过 `dlopen()` 加载库时不会重新初始化库中的静态变量
- **`RTLD_NOLOAD`**(自 glibc 2.2 起):不加载库。这个标记有两个目的
- 第一,可以使用这个标记来检查某个特定的库是否已经被加载到了进程的地址空间中。如果已经加载了,那么 `dlopen()` 会返回库的句柄,如果没有加载,那么` dlopen()` 会返回 `NULL`
- 第二,可以使用这个标记来“提升”已加载的库的标记
- 如在对之前使用 `RTLD_LOCAL` 打开的库调用 `dlopen()` 时可以在 `__mode` 参数中指定 `RTLD_NOLOAD | RTLD_GLOBAL`
- `dlopen()` 在 [[$green]]==**成功时会返回一个句柄**==,在后续对 `dlopen` API 中的函数的调用可以使用该句柄来引用这个库。如果 [[$green]]==**发生了错误**==(如无法找到库),那么 `dlopen()` [[$green]]==**会返回 NULL**==
- [[#blue]]==在同一个库文件中可以**多次调用** `dlopen()`,但将库 **加载进内存的操作只会发生一次(第一次调用)**,所有的调用都返回同样的句柄值==
-`dlopen` API 会为每个库句柄 [[$red]]==**维护一个引用计数**==,每次调用 `dlopen()` 时都会增加引用计数,每次 [[#blue]]==调用 `dlclose()` 都会 **减小引用计数**==,只有当计数为 0 时 `dlclose()` 才会从内存中删除这个库
- ## 获取符号的地址:`dlsym()`
collapsed:: true
- `dlsym()` 函数在 `__handle` 指向的库以及该库的依赖树中的库中搜索名为 `__name` 的符号(函数或变量)
- ```c
void *dlsym (void * restrict __handle, const char * restrict __name);
```
- 如果找到了 `__name`,那么 `dlsym()` 会返回其地址,否则就返回 NULL
- `__handle`参数通常是上一个 `dlopen()` 调用返回的 [[$green]]==**库句柄**==,或者它也可以是 [[$green]]==**伪句柄**==
- ### 在 `dlsym()` 中使用库伪句柄
- `RTLD_DEFAULT` : [[#green]]==从主程序中开始查找 `__name`,接着 **按序在所有已加载的共享库中查找**==,包括那些通过使用了 `RTLD_GLOBAL` 标记的 `dlopen()` 调用动态加载的库,这个标记对应于动态链接器所采用的默认搜索模型
- `RTLD_NEXT` : 在调用 `dlsym()之` 后加载的共享库中搜索 `__name`,这个标记 [[$green]]==**适用于需要创建与在其他地方定义的函数同名的包装函数的情况**==
- 如,在主程序中可能会定义一个 `malloc()`(它可能完成内存分配的簿记工作),而这个函数在调用实际的 `malloc()` 之前首先会通过调用 `func = dlsym(RTLD_NEXT, "malloc")` 来获取其地址
- ## 关闭共享库:`dlclose()`
collapsed:: true
- `dlclose()` 函数关闭一个库
- ```c
int dlclose (void *__handle);
```
- [[#blue]]==`dlclose()` 函数会**减小** `__handle` 所引用的库的打开引用的 **系统计数**==
- [[#green]]==如果这个 **引用计数变成了 0** 并且 **其他库已经不需要用到该库中的符号** 了,那么 **就会卸载这个库**==
- [[$green]]==系统也会在这个库的依赖树中的库执行(递归地)同样的过程==。当进程终止时会隐式地对所有库执行 `dlclose()`
- ## 错误诊断:`dlerror()`
collapsed:: true
- 如果在 `dlopen()` 调用或 `dlopen` API 的其他函数调用中得到了一个错误,那么可以使用 `dlerror()` 来获取一个指向表明错误原因的字符串的指针
- ```c
char *dlerror (void);
```
- 如果从上一次调用 `dlerror()` 到现在没有发生错误,那么 `dlerror()` 函数返回 `NULL`
- ### 示例程序:这个程序接收两个命令行参数:需加载的共享库名称和需执行的库中函数的名称
collapsed:: true
- ```c
/*
动态加载库:dlopen
*/
#include <dlfcn.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char * argv[]) {
void *lib_handle; /* Handle for shared library */
int ( *func_ptr)(int, int); /* Pointer to function with 2 int arguments*/
int result;
const char *err;
if(argc != 3 || strcmp(argv[1], "--help") == 0) {
fprintf(stderr, "%s lib-path func-name\n", argv[0]);
}
/* load the shared library and get a handle for later use */
lib_handle = dlopen(argv[1], RTLD_LAZY);
if (!lib_handle) {
fprintf(stderr, "dlopen: %s\n", dlerror());
}
/* search library for symbol named in argv[2] */
(void) dlerror(); /* clear dlerror()*/
err = dlerror();
func_ptr = dlsym(lib_handle, argv[2]);
if(err) {
fprintf(stderr, "dlsym: %s\n", err);
}
/* if the address returned by dlsym() is no-NULL, try calling it as a function that takes 2 int arguments */
if ( !func_ptr) {
fprintf(stderr, "%s is NULL\n", argv[2]);
}
else {
result = (*func_ptr)(1, 2);
}
printf("call %s, result is %d\n", argv[2], result);
dlclose(lib_handle);
return 0;
}
```
- 测试
- ```shell
[dyp@dyp dll]$ gcc -o dynload dynload.c
[dyp@dyp dll]$ ./dynload ./libdemo.so.1.0.1 add
call add, result is 3
[dyp@dyp dll]$ ./dynload libdemo.so.1.0.1 add
dlopen: libdemo.so.1.0.1: cannot open shared object file: No such file or directory
[dyp@dyp dll]$ LD_LIBRARY_PATH=. ./dynload libdemo.so.1.0.1 add
call add, result is 3
```
- ## 获取与加载的符号相关的信息:`dladdr()`
- `dladdr()`返回一个包含地址 `addr`(通常通过前面的 `dlsym()`调用获得)的相关信息的结构
- ```c
#define _GNU_SOURCE
#include <dlfcn.h>
typedef struct
{
const char *dli_fname; /* 定义对象的文件名 */
void *dli_fbase; /* 该对象的加载地址 */
const char *dli_sname; /* 最近的符号名称 */
void *dli_saddr; /* 最近符号的精确值 */
} Dl_info;
int dladdr (const void *__address, Dl_info *__info);
```
- 要使用这个函数,在 `#include <dlfcn.h>` 之前需要定义一个宏 `_GNU_SOURCE`。否则不能使用该函数
- `__info` 参数是一个指向由调用者分配的结构的指针,其结构形式如下
- ```c
typedef struct
{
const char *dli_fname; /* 共享库的路径名 */
void *dli_fbase; /* 运行时基地址 */
const char *dli_sname; /* 最近的符号名称 */
void *dli_saddr; /* 最近符号的精确值 */
} Dl_info;
```
- `Dl_info` 结构中的前两个字段指定了包含地址 `__address`**共享库的路径名****运行时基地址**。最后两个字段 **返回地址相关的信息**
- 假设 `__address` 指向共享库中一个符号的确切地址,那么 `dli_saddr` 返回的值与传入的 `__address` 值一样
- ## 在主程序中访问符号:`gcc --export–dynamic`
collapsed:: true
- 假设使用 `dlopen()` 动态加载了一个共享库,然后使用 `dlsym()` 获取了共享库中 `x()` 函数的地址,接着调用 `x()`。如果在 `x()` 中调用了函数 `y()`,那么通常会在程序加载的其中一个共享库中搜索 `y()`
- 有些时候需要让 `x()` 调用主程序中的 `y()` 实现(类似于回调机制)。为了达到这个目的就必须要使主程序中的符号(全局作用域)对动态链接器可用,即在链接程序时使用 `−−export−dynamic` 链接器选项
- ```shell
gcc -Wl,--export-dynamic main.c
```
- 或者
- ```shell
gcc -export-dynamic main.c
```
- 使用这些选项中的一个就能够允许动态加载的库访问主程序中的全局符号
- `gcc –rdynamic` 选项和 `gcc –Wl、–E` 选项的含义,以及`–Wl、−−export–dynamic` 是一样的
- # 控制符号的可见性
collapsed:: true
- [[#green]]==设计良好的共享库应该只公开那些构成其声明的应用程序二进制接口(ABI)的符号==(函数和变量),其原因如下
- 如果 [[$green]]==共享库的设计人员不小心导出了未详细说明的接口==,那么使用这个库的应用程序的作者可能会选择使用这些接口。这样在将来 [[$green]]==升级共享库时可能会带来兼容性问题==。库的开发人员认为可以修改或删除那些不属于文档中记录的 ABI 中的接口,而库的用户则希望继续使用名称与他们当前正在使用的接口名称一样的接口(同时语义保持不变)
- 在运行时符号解析阶段,[[$green]]==由共享库导出的所有符号可能会优先于其他共享库提供的相关定义==
- 导出非必需的符号会 [[$green]]==增加在运行时需加载的动态符号表的大小==
- 当库的设计人员确保只导出那些库的声明的 ABI 所需的符号就能使上述问题发生的可能性降到最低或避免上述问题的发生。[[#green]]==下列技术可以用来控制符号的导出==
- [[$green]]==在 C 程序中可以 **使用 static 关键词** 使得一个符号私有于一个源代码模块==,从而使得它无法被其他目标文件绑定
- GNU C 编译器 gcc 提供了一个特有的特性声明,它执行与 static 关键词类似的任务
- ```c
void __attribute__ ((visibility("hidden"))) func (void) {
/* code */
}
```
- `static` 关键词将一个符号的可见性限制在单个源代码文件中,而 [[#blue]]==`hidden` 特性使得一个符号 **对构成共享库的所有源代码文件都可见**,但 **对库之外的文件不可见**==
- [[$red]]==**版本脚本**== 可以用来精确控制符号的可见性以及选择将一个引用绑定到符号的哪个版本
collapsed:: true
- ((65000492-81ff-4609-bbeb-8ea00c75b906))
- 当动态加载一个共享库时,`dlopen()` 接收的 `RTLD_GLOBAL` 标记可以用来指定这个库中定义的符号应该用于后续加载的库中的绑定操作,`--export–dynamic`链接器选项可以用来使主程序的全局符号对动态加载的库可用
- # 链接器版本脚本
- [[$green]]==**版本脚本** 是一个包含 **链接器** `ld` 执行的 **指令的文本文件**==。要使用版本脚本必须要指定`--version–script` 链接器选项
- ```shell
gcc -Wl,--version-script,myscriptfile.map ...
```
- [[#green]]==**版本脚本** 的后缀通常(但不统一)是 **.map**==
- ## 使用版本脚本控制符号的可见性
id:: 65000492-81ff-4609-bbeb-8ea00c75b906
- [[#red]]==版本脚本的一个用途是 **控制那些可能会在无意中变成全局可见的符号(即对与该库进行链接的应用程序可见)的可见性**==
- 举一个简单的例子,假设需要从三个源文件 `vis_comm.c``vis_f1.c` 以及 `vis_f2.c` 中构建一个共享库,这三个源文件分别定义了函数 `vis_comm()``vis_f1()`以及 `vis_f2()``vis_comm()`函数由 `vis_f1()``vis_f2()` 调用,但不想被与该库进行链接的应用程序直接使用。再假设使用常规的方式来构建共享库
-
-
-
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册