2.3 携程的hook实现及dlsym函数
背景知识:(排除static 情况)
一个进程中可以有相同的命名吗? -- 不能
两个进程之间可以有相同的命名吗?--可以
一个进程和另一个静态库可以有相同的命名吗?--不能
一个进程和另一个动态库之间可以有相同的命名吗?--可以
在考虑 进程 与 静态库 或 动态库 中的函数与变量命名是否可以相同时,需要从作用域、链接阶段、加载机制等角度进行分析。以下分别讨论静态库和动态库的情况:
1. 进程与静态库之间的函数或变量命名是否可以相同?
可以,但需注意链接时的冲突。
原因:
- 静态库的性质:静态库(
.a
文件)在编译阶段被链接器整合到可执行文件中,其代码最终成为可执行文件的一部分。因此,静态库中的函数或变量在链接时会与进程(可执行文件)的代码共享同一命名空间。 - 冲突情况:
- 如果进程代码中定义了与静态库相同的函数或变量名,链接器会发生符号重定义错误,除非通过弱符号等机制处理。
- 如果名称不同,则不会产生冲突。
示例:
-
正常情况:
- 静态库
libexample.a
中定义了函数void foo()
。 - 可执行文件中没有定义
foo()
,则链接器会将foo()
从静态库中包含到可执行文件中。
- 静态库
-
冲突情况:
- 如果静态库和可执行文件都定义了
void foo()
,链接器会报错:duplicate symbol: foo
- 如果静态库和可执行文件都定义了
-
解决方法:
- 避免冲突:确保静态库和可执行文件的全局命名空间中没有重复定义。
- 使用弱符号:通过
__attribute__((weak))
标记静态库的符号为弱符号,让进程中的定义优先。
2. 进程与动态库之间的函数或变量命名是否可以相同?
可以,但需注意运行时的符号解析规则。
原因:
- 动态库的性质:动态库(
.so
文件)中的代码在运行时由动态链接器加载到进程中。如果动态库与可执行文件存在同名函数或变量,符号解析规则决定了哪个定义会被使用。
符号解析规则:
-
默认规则:
- 可执行文件中的符号优先于动态库中的符号。
- 如果可执行文件没有定义某个符号,则动态库中的定义会被使用。
-
显式控制规则:
- 使用
--export-dynamic
编译选项,可以让可执行文件中的符号在动态链接器中被导出,从而覆盖动态库中的同名符号。 - 使用动态库加载函数(如
dlopen()
和dlsym()
)时,可以显式指定使用动态库中的符号。
- 使用
示例:
-
正常情况:
- 动态库
libexample.so
中定义了函数void bar()
。 - 可执行文件中没有定义
bar()
,则运行时会使用动态库中的bar()
。
- 动态库
-
冲突情况:
- 如果动态库和可执行文件都定义了
void bar()
,运行时会优先使用可执行文件中的bar()
,即使动态库的bar()
被链接到进程中。
- 如果动态库和可执行文件都定义了
-
解决方法:
- 避免冲突:为动态库中的符号使用独立的命名空间(如通过
namespace
或static
修饰)。 - 显式调用:通过
dlsym()
显式加载动态库中的函数,绕过默认的符号解析规则。
- 避免冲突:为动态库中的符号使用独立的命名空间(如通过
3. 总结
对象关系 | 函数/变量是否可以相同 | 冲突可能性 | 解决方案 |
---|---|---|---|
进程和静态库 | 可以 | 链接时可能冲突 | 避免符号冲突或使用弱符号机制 |
进程和动态库 | 可以 | 运行时可能覆盖 | 避免符号冲突、显式调用动态库的符号或隔离命名空间 |
4. 建议
-
命名空间隔离:
- 使用独特的命名或为库定义专属命名空间(如 C++ 的
namespace
或 C 的命名前缀)。
- 使用独特的命名或为库定义专属命名空间(如 C++ 的
-
弱符号机制:
- 为静态库的符号使用
__attribute__((weak))
,以便在进程中定义相同符号时优先使用进程的实现。
- 为静态库的符号使用
-
动态链接控制:
- 对动态库,尽量避免与可执行文件使用相同的全局符号。
- 在需要显式控制时,使用
dlsym()
指定加载的符号。
这样可以有效避免命名冲突导致的链接错误或运行时行为异常。
如果一个函数位于我们需要链接的动态库中,则可以用dlsym函数把他hook住,同时我们在自己的进程中也可以再定义一个相同名字的函数,则可以实现动态库的函数"重载"
dlsym函数的作用
`dlsym()` 和 `dlvsym()` 都是 Linux 动态链接库(`dl`)的一部分,用于在运行时动态加载库和获取符号(函数或变量)地址。它们主要用于实现**动态库**(`shared libraries`)的加载和符号解析。
### `dlsym()` 函数:
- **功能**:`dlsym()` 用于在指定的动态库中查找符号地址(函数或变量)。它通常和 `dlopen()` 配合使用,通过动态加载库后,使用 `dlsym()` 来获取某个符号的地址,然后可以通过这个地址调用动态库中的函数。
- **参数**:
- `handle`:这是通过 `dlopen()` 获得的库句柄。
- `symbol`:要查找的符号名称(字符串形式)。
- **返回值**:成功时返回符号的地址,失败时返回 `NULL`。
- **示例**:
```c
void *handle = dlopen("libm.so", RTLD_LAZY);
double (*cos_func)(double) = (double (*)(double)) dlsym(handle, "cos");
```
在这个例子中,`dlsym()` 获取了动态库 `libm.so` 中 `cos` 函数的地址。
### `dlvsym()` 函数:
- **功能**:`dlvsym()` 与 `dlsym()` 类似,但它允许指定符号的**版本号**。这是用于那些支持符号版本控制的库。比如,当一个动态库的不同版本中同一个符号有不同实现时,可以使用 `dlvsym()` 指定加载特定版本的符号。
- **参数**:
- `handle`:同样是由 `dlopen()` 返回的句柄。
- `symbol`:要查找的符号名称。
- `version`:符号的版本号。
- **返回值**:成功时返回符号的地址,失败时返回 `NULL`。
例如,如果一个库在不同版本中定义了同名的函数,但行为不同,可以通过指定 `version` 参数,确保加载特定版本的符号。
### 区别:
- `dlsym()` 只查找符号,而 `dlvsym()` 允许指定符号的版本,这在符号有多个版本的库中尤为重要。
### 使用场景:
- **`dlsym()`** 常用于插件系统或动态库调用,能够在运行时决定调用的函数。
- **`dlvsym()`** 通常在库设计复杂、符号有多个版本时使用,确保兼容性和特定功能。
两者的配合使用可以让程序在运行时根据需求动态选择不同的库函数,从而增强灵活性。
dlsym() 常用于插件系统或动态库调用,能够在运行时决定调用的函数。这个怎么来理解
`dlsym()` 的功能可以通过“插件系统”或“动态库调用”来理解。这涉及到在程序运行时动态加载某个库,选择具体的函数进行调用,而不是在编译时就确定要调用的函数。通过这种机制,程序可以在运行时灵活选择功能,实现更高的可扩展性和灵活性。
### 具体理解:
1. **插件系统**:
插件系统是一种软件设计模式,允许程序通过外部插件扩展其功能,而无需在程序主框架中直接包含这些功能。在插件系统中,每个插件通常是一个动态链接库(如 `.so` 文件)。当程序运行时,它会动态加载这些库,找到特定的符号(函数或变量)来执行插件的功能。
**示例**:
想象一个文本编辑器支持不同的文件格式,比如 `.txt`、`.md`、`.html`。文本编辑器的核心程序不知道所有这些格式的具体操作逻辑,而是允许插件来实现。插件通过动态库的形式提供特定功能,编辑器使用 `dlopen()` 动态加载插件,再用 `dlsym()` 查找并调用插件中处理这些文件的函数。
```c
void *handle = dlopen("markdown_plugin.so", RTLD_LAZY);
void (*convert_to_html)(const char *) = dlsym(handle, "convert_to_html");
if (convert_to_html) {
convert_to_html("example.md");
}
```
在这个例子中,主程序不知道如何处理 `.md` 文件,但通过 `dlsym()` 动态加载插件中的 `convert_to_html` 函数,它可以在运行时决定是否调用特定的插件函数。
2. **动态库调用**:
动态库调用的核心思想是程序在运行时决定要使用的库函数,而不是在编译时将库函数硬编码到程序中。通过 `dlsym()`,程序可以灵活选择不同的实现。
**动态库的好处**:
- **节省资源**:不需要将所有库函数都加载到内存中,只有在需要时才动态加载,节省内存资源。
- **灵活扩展**:可以随时添加或替换动态库,而不需要重新编译程序。比如,一个图像处理程序可以通过 `dlsym()` 在运行时选择使用 `libjpeg` 还是 `libpng` 来处理不同的图片格式。
3. **运行时决定调用的函数**:
通常在程序中,函数调用是在编译时就确定好的。但通过 `dlsym()`,程序可以在运行时动态决定调用哪个函数。这个机制常用于需要高扩展性、功能多样性或者有插件需求的程序。
**应用场景**:
- **数据库驱动**:许多数据库应用支持多种数据库引擎(如 MySQL、PostgreSQL 等),通过 `dlsym()`,程序可以动态选择要使用的数据库驱动。
- **图形库或音频库**:可以动态选择使用不同的图形或音频库,依赖不同的库文件来支持不同的功能。
总结来说,`dlsym()` 通过在运行时决定要调用的函数,使得程序在无需重新编译的情况下能够加载不同的模块或插件,从而实现动态加载和功能扩展。
dlsym函数hook write函数
这段代码的主要作用是在拦截和包装系统调用 `write()`,通过动态链接库机制重定向它的执行。下面是详细的解释:
### 1. **自定义 `write` 函数:**
```c
ssize_t write(int fd, const void *buf, size_t count) {
printf("write: %s\n", (const char *)buf);
return write_f(fd, buf, count);
}
```
- 这个 `write()` 函数是一个自定义的 `write`,与系统提供的 `write` 函数同名。
- 它首先输出一个调试信息 `write: %s\n`,将要写入的数据(假设是字符串)打印出来。这用于调试,帮助开发者看到传入的字符串内容。
- 然后它调用了 `write_f(fd, buf, count)`,这是真正执行写操作的函数。
### 2. **使用 `dlsym()` 获取原始 `write` 函数:**
```c
if (!write_f) {
write_f = dlsym(RTLD_NEXT, "write");
}
```
- 这部分代码通过 `dlsym(RTLD_NEXT, "write")` 获取系统中原始的 `write()` 函数地址,并将其存储在 `write_f` 中。
- **`RTLD_NEXT`** 是一个特殊的标志,它告诉 `dlsym()` 查找下一个符号,跳过当前动态库,找到真正的系统 `write()` 函数。这样避免了递归调用自己定义的 `write` 函数,导致死循环。
- `write_f` 变量会缓存 `dlsym()` 找到的系统调用地址,以避免每次调用 `write()` 时都使用 `dlsym()`,提高性能。
### 3. **作用和应用场景:**
- **函数拦截**:这段代码实现了函数拦截技术,用于拦截程序中的 `write()` 系统调用,然后进行额外的处理,比如打印调试信息或修改写入数据。
- **库注入与动态分析**:这种技术常用于调试和性能分析工具,如 `strace` 或 `ld_preload` 技术中,可以通过拦截某些系统调用来观察程序行为。
- **调试与日志**:通过 `printf()` 打印出写入的数据内容,可以帮助开发者在不修改程序源代码的情况下追踪 `write()` 调用。
### 总结:
这段代码的作用是通过 `dlsym()` 拦截并扩展系统调用 `write()` 的功能,在调用系统 `write()` 之前进行一些自定义操作,比如输出调试信息,然后再调用原始的 `write()` 实现写操作。这种方法在需要监控、记录或修改系统调用行为时非常有用。
示例
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>ssize_t (*read_f)(int fd, void *buf, size_t count);
ssize_t (*write_f)(int fd, const void *buf, size_t count);ssize_t read(int fd, void *buf, size_t count)
{read_f = dlsym(RTLD_NEXT, "read"); // RTLD_NEXT 表示跳过自己当前程序中定义的read函数if (read_f == NULL) {return -1;}puts("my hook read");return read_f(fd, buf, count);
}int main(int argc, char *argv[])
{system("echo -n \"this is a test file!\" > test.file");int fd = open("test.file", O_RDONLY);if (fd == -1) {perror("open err");return -1;}char buf[100] = {0};if (read(fd, buf, 99) != -1) {puts(buf);close(fd);}system("rm ./test.file");return 0;
}
Sign in · GitLab