问题

是否有办法在malloc的hook函数中调用原生malloc?

回答
是的,在 `malloc` 的 hook 函数中调用原生 `malloc` 是可行且常见的做法。这通常是为了实现一些高级功能,例如:

内存追踪和监控: 记录每次内存分配的大小、地址、调用栈等信息,用于性能分析或内存泄漏检测。
内存池管理: 实现自定义的内存分配策略,例如预分配一定大小的内存块,或者对特定大小的内存进行缓存。
内存安全增强: 在分配内存前后进行额外的检查,例如在内存末尾添加保护字节(guard bytes)来检测缓冲区溢出。
调试和断言: 在分配失败时打印更详细的错误信息,或者在分配的内存区域添加特定的标记以便调试。

要实现这一点,你需要理解 `malloc` hook 的工作原理以及 C 语言的函数重定向机制。下面我将详细介绍几种常用的方法。

核心思想:函数重定向

`malloc` hook 的核心在于拦截对 `malloc` 的直接调用。当你的程序尝试调用 `malloc` 时,你希望的是执行你的自定义函数,而不是系统默认的 `malloc` 函数。在你的自定义函数内部,你可以执行一些操作,然后再显式地调用原始的 `malloc` 函数来完成实际的内存分配。

关键是如何“指向”原始的 `malloc` 函数。以下是几种主要的方法:

方法一:使用 `dlsym` 和 `RTLD_NEXT` (最常用和推荐的方法,主要用于动态链接库)

这种方法是现代 C/C++ 开发中最常用、最灵活且推荐的方式,尤其适用于编写动态链接库(.so 或 .dll)。

原理:

动态链接: 你的程序在运行时会加载动态链接库。当程序调用一个函数时(例如 `malloc`),动态链接器会在加载的库中查找该函数的实现。
`dlsym`: 这是一个 POSIX 标准函数,用于在动态链接器提供的句柄(通常是 `RTLD_DEFAULT` 或 `RTLD_SELF`)中查找符号(函数名或变量名)的地址。
`RTLD_NEXT`: 这是一个特殊的句柄,告诉 `dlsym` 在当前符号的下一个(即比当前链接顺序更靠后的库中)查找指定符号。如果你的 hook 函数定义在一个动态库中,并且这个库被链接到主程序中,那么使用 `RTLD_NEXT` 就能找到主程序或更早加载的库中定义的原始 `malloc` 实现。

实现步骤:

1. 创建 hook 函数: 定义一个与 `malloc` 函数签名相同的函数,例如 `my_malloc`。
2. 获取原始 `malloc` 地址: 在你的 hook 函数内部(或者更早初始化时),使用 `dlsym(RTLD_NEXT, "malloc")` 来获取原始 `malloc` 函数的地址。将这个地址存储在一个全局指针变量中。
3. 执行自定义逻辑: 在 `my_malloc` 中,你可以执行你的内存追踪、检查或其他操作。
4. 调用原始 `malloc`: 使用存储的全局指针,调用原始的 `malloc` 函数,并将参数传递过去,然后返回其结果。
5. 强制链接: 确保你的 hook 函数在主程序链接时优先于系统的 `malloc`。通常通过在编译时使用 `Wl,wrap=malloc` 选项来实现。这个选项会指示链接器将所有对 `malloc` 的调用重定向到 `__wrap_malloc`,而 `__real_malloc` 则是原始 `malloc` 的别名。你的 `__wrap_malloc` 函数会调用 `__real_malloc`。

示例代码 (Linux/macOS):

hook.c:

```c
define _GNU_SOURCE // For RTLD_NEXT
include
include
include
include // For getpid()

// 声明一个指向原始 malloc 函数的指针
static void (real_malloc)(size_t) = NULL;

// 你的 hook 函数
void malloc(size_t size) {
if (real_malloc == NULL) {
// 首次调用时,找到原始的 malloc 函数
// RTLD_NEXT 会在比当前库更靠后的库中查找 "malloc"
real_malloc = dlsym(RTLD_NEXT, "malloc");
if (real_malloc == NULL) {
fprintf(stderr, "Error in dlsym: %s ", dlerror());
exit(EXIT_FAILURE);
}
}

//
// 在这里执行你的自定义逻辑
printf("[HOOK] malloc(%zu) called. PID: %d ", size, getpid());
//

// 调用原始的 malloc 函数并返回其结果
void ptr = real_malloc(size);

//
// 可以在分配后再次执行自定义逻辑
printf("[HOOK] malloc(%zu) returned %p ", size, ptr);
//

return ptr;
}

// 可选:也可以 hook free, calloc, realloc 等
void calloc(size_t nmemb, size_t size) {
static void (real_calloc)(size_t, size_t) = NULL;
if (real_calloc == NULL) {
real_calloc = dlsym(RTLD_NEXT, "calloc");
if (real_calloc == NULL) {
fprintf(stderr, "Error in dlsym for calloc: %s ", dlerror());
exit(EXIT_FAILURE);
}
}
printf("[HOOK] calloc(%zu, %zu) called. ", nmemb, size);
return real_calloc(nmemb, size);
}

void free(void ptr) {
static void (real_free)(void) = NULL;
if (real_free == NULL) {
real_free = dlsym(RTLD_NEXT, "free");
if (real_free == NULL) {
fprintf(stderr, "Error in dlsym for free: %s ", dlerror());
exit(EXIT_FAILURE);
}
}
printf("[HOOK] free(%p) called. ", ptr);
real_free(ptr);
}

// ... 其他内存函数 hook ...
```

主程序 (main.c):

```c
include
include

int main() {
printf("Starting main program... ");
void mem1 = malloc(100);
void mem2 = calloc(5, 20);
printf("Allocated mem1: %p, mem2: %p ", mem1, mem2);
free(mem1);
free(mem2);
printf("Main program finished. ");
return 0;
}
```

编译和运行:

1. 编译 hook 函数为动态库:
```bash
gcc shared fPIC hook.c o libhook.so ldl
```
`shared`: 生成共享库。
`fPIC`: 生成位置无关代码,这是动态库的必需选项。
`ldl`: 链接 `libdl` 库,其中包含 `dlsym`。

2. 编译主程序:
```bash
gcc main.c o main_program
```

3. 运行主程序,并强制加载 hook 库:
```bash
export LD_PRELOAD=/path/to/libhook.so Linux
export DYLD_INSERT_LIBRARIES=/path/to/libhook.so macOS
./main_program
```
`LD_PRELOAD` (Linux) 或 `DYLD_INSERT_LIBRARIES` (macOS) 是环境变量,它会告诉动态链接器在加载程序时优先加载指定的共享库。这样,当主程序调用 `malloc` 时,它会先找到在 `libhook.so` 中定义的 `malloc` 函数。

输出示例:

```
Starting main program...
[HOOK] malloc(100) called. PID: 12345
[HOOK] malloc(100) returned 0x567890abcdef
[HOOK] calloc(5, 20) called.
[HOOK] malloc(100) called. PID: 12345 // calloc 内部也可能调用 malloc
[HOOK] malloc(100) returned 0x567890abc0
[HOOK] free(0x567890abcdef) called.
[HOOK] free(0x567890abc0) called.
Main program finished.
```

为什么这是推荐方法?

解耦: hook 逻辑与主程序代码完全解耦,可以独立开发和测试。
易于集成: 通过 `LD_PRELOAD` 可以在不修改主程序编译命令的情况下轻松启用/禁用 hook。
平台兼容性: `dlsym` 和 `RTLD_NEXT` 是 POSIX 标准的一部分,在大多数 Unixlike 系统上有效。



方法二:使用链接器的 `wrap` 选项 (`Wl,wrap=malloc`)

这种方法不依赖 `dlsym`,而是直接通过编译器和链接器来处理函数重定向。它通常是静态链接库 hook 或在特定编译环境下的首选。

原理:

当使用 `Wl,wrap=symbol_name` 选项编译时,链接器会将所有对 `symbol_name` 的调用重定向到一个名为 `__wrap_symbol_name` 的函数。同时,原始的 `symbol_name` 函数会被重命名为 `__real_symbol_name`。你的任务就是提供 `__wrap_symbol_name` 函数,并在其中调用 `__real_symbol_name`。

实现步骤:

1. 创建 hook 函数: 定义一个名为 `__wrap_malloc` 的函数,它具有与 `malloc` 相同的签名。
2. 直接调用原始函数: 在 `__wrap_malloc` 中,直接调用 `__real_malloc` 来完成实际的内存分配。
3. 编译时添加选项: 在编译和链接你的主程序时,加入 `Wl,wrap=malloc`(以及其他你想要 hook 的函数,如 `free`)。

示例代码 (compilerwrap.c):

```c
include
include
include // For getpid()

// 声明原始 malloc 函数,链接器会将其重命名为 __real_malloc
extern void __real_malloc(size_t size);
extern void __real_calloc(size_t nmemb, size_t size);
extern void __real_free(void ptr);

// 你的 hook 函数,链接器会将所有对 malloc 的调用指向这里
void __wrap_malloc(size_t size) {
//
// 在这里执行你的自定义逻辑
printf("[WRAP] malloc(%zu) called. PID: %d ", size, getpid());
//

// 调用原始的 malloc 函数
void ptr = __real_malloc(size);

//
// 可以在分配后再次执行自定义逻辑
printf("[WRAP] malloc(%zu) returned %p ", size, ptr);
//

return ptr;
}

void __wrap_calloc(size_t nmemb, size_t size) {
printf("[WRAP] calloc(%zu, %zu) called. ", nmemb, size);
return __real_calloc(nmemb, size);
}

void __wrap_free(void ptr) {
printf("[WRAP] free(%p) called. ", ptr);
__real_free(ptr);
}
```

主程序 (main_wrap.c):

```c
include
include

int main() {
printf("Starting main program... ");
void mem1 = malloc(100);
void mem2 = calloc(5, 20);
printf("Allocated mem1: %p, mem2: %p ", mem1, mem2);
free(mem1);
free(mem2);
printf("Main program finished. ");
return 0;
}
```

编译和运行:

```bash
将 hook 函数编译成一个目标文件
gcc c compilerwrap.c o compilerwrap.o

编译主程序,并在链接时使用 wrap 选项
将你的 hook 对象文件与主程序一起链接
gcc main_wrap.c compilerwrap.o o main_program_wrap Wl,wrap=malloc Wl,wrap=calloc Wl,wrap=free
```

输出示例:

```
Starting main program...
[WRAP] malloc(100) called. PID: 54321
[WRAP] malloc(100) returned 0x567890abc0
[WRAP] calloc(5, 20) called.
[WRAP] malloc(100) called. PID: 54321 // calloc 内部也可能调用 malloc
[WRAP] malloc(100) returned 0x567890abc1
[WRAP] free(0x567890abc0) called.
[WRAP] free(0x567890abc1) called.
Main program finished.
```

优点:

无需动态库: 如果你只想在特定编译目标上启用 hook,或者不希望引入额外的动态库依赖,这是个不错的选择。
更底层的控制: 直接通过链接器控制,可以应用于更广泛的场景。

缺点:

需要修改编译命令: 每次编译都需要显式添加 `wrap` 选项。
hook 代码需要与主程序一起链接: 无法像 `LD_PRELOAD` 那样独立于主程序。



方法三:手动覆盖标准库的 `malloc` (不推荐,易出错)

这是最直接但也最不推荐的方法。理论上,你可以直接在你的代码中定义一个与 `malloc` 同名的函数,但这样做会遇到许多问题。

原理:

直接在你的代码中定义一个函数,名称为 `malloc`。编译器会认为这是你自己的实现,并且会生成对这个函数的调用。

遇到的问题:

1. 如何调用原始 `malloc`?
这是最大的问题。一旦你定义了自己的 `malloc`,你无法直接调用系统提供的那个原始的 `malloc`。标准库的实现通常是链接器根据内部机制来提供的,你没有一个明确的“入口点”可以指向它。
你可能会尝试使用 `dlsym(RTLD_NEXT, "malloc")`,但如果在你的 hook 函数(现在就叫 `malloc`)内部调用 `dlsym`,它可能会找到你自己定义的 `malloc` 函数(也就是你自己!),导致无限递归。
即使你尝试使用 `dlsym(RTLD_DEFAULT, "malloc")`,在某些情况下也可能获取到你自己定义的函数的地址,而不是真正的系统 `malloc`。

2. 符号冲突:
如果你的程序链接了其他库,而这些库也依赖于标准库的 `malloc`,那么你的自定义 `malloc` 可能会与它们产生冲突。

3. 标准库的内部调用:
很多标准库的内部函数(例如 `printf` 的某些实现)可能会直接调用 `malloc`,而不会通过一个可重定向的符号。你的自定义 `malloc` 可能无法被这些内部调用正确地识别或覆盖。

结论:

不推荐使用这种方法。它在技术上很难实现,并且容易导致运行时错误或无限递归。



其他注意事项和高级技巧:

Hook `free`, `calloc`, `realloc` 等: 通常,如果你 hook 了 `malloc`,你也需要 hook `free`、`calloc`、`realloc` 等其他内存管理函数,以确保整个内存管理过程都在你的监控之下。
初始化顺序: 使用 `dlsym` 时,确保你是在第一次调用 `malloc` 之前获得了 `real_malloc` 的指针。通常在 hook 函数的静态变量初始化或第一次调用时进行。
线程安全: 你的 hook 函数需要是线程安全的,尤其是在全局变量(如 `real_malloc` 指针)的访问和修改时。通常可以使用互斥锁来保护。
性能影响: 每次 hook 调用都会增加额外的函数调用和打印输出,这会显著影响内存分配的性能。在生产环境中需要谨慎使用,并考虑仅在调试或特定模式下启用。
递归保护: 在你的 hook 函数中,尽量避免在执行自定义逻辑时再次调用其他会被 hook 的函数,除非你明确地打算这么做(例如通过 `dlsym` 获取原始函数)。否则容易导致无限递归。
内存泄漏检测: 如果你的 hook 函数本身也使用了 `malloc`,并且没有妥善处理,可能会导致内存泄漏或栈溢出。因此,建议 hook 函数中的内存分配尽量少,或者使用一个独立的、非 hook 的内存分配器(例如,如果你的 hook 是一个动态库,你可以尝试在 hook 库内部使用 `dlopen` 加载一个系统库的副本,并从中获取 `malloc` 的地址,但这会更复杂)。
Windows 上的实现: 在 Windows 上,可以使用 `Detours` 库或其他类似的函数挂钩技术来实现 `malloc` 的 hook。原理类似,但 API 不同。`Detours` 是 Microsoft 提供的工具,可以用于函数拦截。

总结

在 `malloc` 的 hook 函数中调用原生 `malloc` 是完全可行的,并且是实现内存监控、管理和调试的常用技术。

对于动态链接库和灵活的运行时 hook,`dlsym` 配合 `RTLD_NEXT` 和 `LD_PRELOAD` 是最推荐和最通用的方法。
对于特定编译目标或希望不依赖动态库的场景,链接器的 `wrap` 选项是另一种有效的方式。

选择哪种方法取决于你的具体需求和项目环境。无论哪种方法,都要注意线程安全、初始化顺序和潜在的性能影响。

网友意见

user avatar

可以参考 visual leak detect这个项目的思路,很轻量的方案。

类似的话题

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有