拦截malloc、free等库函数(malloc钩子)

本文详细介绍了glibc中malloc_hook的使用方法及其限制条件,特别是针对多线程环境下的注意事项。通过具体示例展示了如何利用malloc_hook进行内存分配调试。

注意,以下方法对于多线程来说不管用。如,A线程调用到malloc_hook 函数,把malloc_hook函数还原为libc malloc函数,此时B线程调用到malloc函数便是libc malloc函数,根本不会进入到我们的malloc_hook设定的my_malloc函数。因此,多线程下是不能使用此方法,需要用__wrap_malloc方式解决。


__malloc_hook是一组glibc提供的malloc调试变量中的一个,这组变量包括:

void *(*__malloc_hook)(size_t size, const void *caller);

void *(*__realloc_hook)(void *ptr, size_t size, const void *caller);

void *(*__memalign_hook)(size_t alignment, size_t size, const void *caller);

void (*__free_hook)(void *ptr, const void *caller);

void (*__malloc_initialize_hook)(void);

void (*__after_morecore_hook)(void);

何谓钩子函数,当一个函数挂载了钩子函数后,你执行这个函数时,实际执行的是钩子函数

只要你在程序中写上”__malloc_hook= my_malloc_hook;”,之后的malloc调用都会使用my_malloc_hook函数,方便易行。但是这组调试变量不是线程安全的,当你想用系统malloc的时候不得不把他们改回来,多线程调用就得上锁。

#include <stdio.h>
#include <malloc.h>

#define THREAD_AUTOLOCK

static void (*old_free)(void *ptr, const void *caller);
static void *(*old_malloc)(size_t size, const void *caller);

static void my_free(void *ptr, const void *caller);
static void *my_malloc(size_t size, const void *caller);

static void hook_back()
{
    old_malloc = __malloc_hook;
    old_free = __free_hook;
}

static void hook_init()
{
    __malloc_hook = my_malloc;
    __free_hook = my_free;
}

static void hook_restore()
{
    __malloc_hook = old_malloc;
    __free_hook = old_free;
}

static void my_free(void *ptr, const void *caller)
{
    THREAD_AUTOLOCK;
    hook_restore();// 这里必须,不然会死循环
    printf("free address: %x\n", ptr);
    free(ptr);
    hook_init();
    return;
}

static void *my_malloc(size_t size, const void *caller)
{
    THREAD_AUTOLOCK;
    void *p = NULL;
    hook_restore();// 这里必须,不然会死循环
    printf("malloc size: %d\n", size);
    p = malloc(size);
    hook_init();
    return p;
}

static void my_mempool_destroy()
{
    hook_restore();
}

static void my_mempool_init()
{
    hook_back();
    hook_init();
    atexit(my_mempool_destroy);
}

void (*__MALLOC_HOOK_VOLATILE __malloc_initialize_hook) (void) = my_mempool_init;
int main(int argc, char **argv)
{
    void *p = malloc(12);
    if (p != NULL)
        free(p);
    return 0;
}

int atexit(void(*func)(void));

    其中,atexit的参数是一个函数地址,当调用此函数时无须传递任何参数,该函数也不能返回值,atexit函数称为终止处理程序注册程序,注册完成以后,当函数终止是exit()函数会主动的调用前面注册的各个函数,但是exit函数调用这些函数的顺序于这些函数登记的顺序是相反的,我认为这实质上是参数压栈造成的,参数由于压栈顺序而先入后出。同时如果一个函数被多次登记,那么该函数也将多次的执行。
    #include<stdio.h>
    #include<stdlib.h>

    void func1(void)
    {
            printf("in func1\n");
    }

    void func2(void)
    {
            printf("in func2\n");
    }

    void func3(void)
    {
            printf("in func3\n");
    }

    int main()
    {
            atexit(func3);
            atexit(func2);
            atexit(func1);

            printf("In main\n");

            return 0;
    }

exit()和_exit()以及_Exit()函数的本质区别是是否立即进入内核,_exit()以及_Exit()函数都是在调用后立即进入内核,而不会执行一些清理处理,但是exit()则会执行一些清理处理,这也是为什么会存在atexit()函数的原因,因为exit()函数需要执行清理处理,需要执行一系列的操作,这些终止处理函数实际上就是完成各种所谓的清除操作的实际执行体。atexit函数的定义也给了程序员一种运用exit执行一些清除操作的方法,比如有一些程序需要额外的操作,具体的清除操作可以采用这种方法对特殊操作进行清除等。
<think>我们正在讨论一个关于动态库方式替换malloc/calloc时无法捕捉调用的问题。用户提到在使用动态库替换这些函数时遇到了无法捕捉调用的问题。根据引用[1]和引用[2],我们知道malloc、calloc、realloc等函数是动态内存分配的基础,并且它们的失败必须被检查,否则会导致严重问题。 用户的问题可能是:在尝试通过动态库替换标准库中的malloc和calloc函数时,发现替换没有生效,或者无法捕捉到对这些函数的调用。这通常发生在动态链接的环境中,可能的原因包括链接顺序、符号覆盖问题,或者程序内部直接使用了其他内存分配函数。 根据引用[1],我们了解到malloc、calloc和realloc的区别。引用[2]则强调了检查这些函数返回值的重要性,以避免空指针解引用等安全问题。 解决这个问题,我们需要考虑动态链接库中符号替换的机制。在Linux系统中,使用LD_PRELOAD环境变量可以预加载一个动态库,从而覆盖标准库中的符号。但是,如果替换没有生效,可能的原因包括: 1. 动态库没有正确导出malloc、calloc等符号。 2. 程序内部直接使用了其他内存分配函数(如realloc、aligned_alloc等),而这些函数没有被替换。 3. 链接顺序问题,可能标准库在预加载库之后被加载,导致预加载的符号被覆盖(这通常不会发生,因为预加载库的符号优先级更高)。 4. 程序使用了静态链接,或者使用了其他内存分配库(如jemalloc、tcmalloc)等。 为了捕捉对malloc/calloc的调用,我们可以通过LD_PRELOAD加载一个自定义的动态库,该动态库中实现了malloc、calloc等函数,并在其中加入日志或钩子(hook)功能。但是,如果这些调用没有被捕捉到,可能是由于上述原因之一。 下面,我们将逐步分析并解决这个问题。 ### 步骤1:确保动态库正确导出符号 自定义动态库必须正确定义并导出malloc、calloc等函数。例如,一个简单的动态库示例: ```c // mymalloc.c #include <stdio.h> #include <stdlib.h> #include <dlfcn.h> void *malloc(size_t size) { static void *(*real_malloc)(size_t) = NULL; if (!real_malloc) { real_malloc = dlsym(RTLD_NEXT, "malloc"); } void *ptr = real_malloc(size); printf("malloc(%zu) = %p\n", size, ptr); return ptr; } void *calloc(size_t num, size_t size) { static void *(*real_calloc)(size_t, size_t) = NULL; if (!real_calloc) { real_calloc = dlsym(RTLD_NEXT, "calloc"); } void *ptr = real_calloc(num, size); printf("calloc(%zu, %zu) = %p\n", num, size, ptr); return ptr; } ``` 编译动态库: ```bash gcc -shared -fPIC -o libmymalloc.so mymalloc.c -ldl ``` ### 步骤2:使用LD_PRELOAD加载动态库 运行程序时,使用: ```bash LD_PRELOAD=./libmymalloc.so ./your_program ``` ### 步骤3:检查是否捕捉到调用 如果此时仍然没有捕捉到调用,可能的原因有: - 程序内部使用了其他内存分配函数(如realloc、aligned_alloc等),需要我们在动态库中也替换这些函数。 - 程序可能直接使用了系统调用(如brk或mmap),但这种情况较少见,因为标准库函数通常都是基于这些系统调用。 - 程序可能静态链接了内存分配函数,此时LD_PRELOAD无法替换静态链接的符号。 ### 步骤4:扩展动态库以覆盖更多函数 我们可以将其他内存分配函数也进行替换,例如realloc、free、aligned_alloc等。 ```c void *realloc(void *ptr, size_t size) { static void *(*real_realloc)(void *, size_t) = NULL; if (!real_realloc) { real_realloc = dlsym(RTLD_NEXT, "realloc"); } void *new_ptr = real_realloc(ptr, size); printf("realloc(%p, %zu) = %p\n", ptr, size, new_ptr); return new_ptr; } void free(void *ptr) { static void (*real_free)(void *) = NULL; if (!real_free) { real_free = dlsym(RTLD_NEXT, "free"); } real_free(ptr); printf("free(%p)\n", ptr); } ``` ### 步骤5:处理C++中的操作符new和delete 如果程序是C++编写的,那么new和delete操作符也会调用内存分配函数。但是,这些操作符在C++中通常是通过mallocfree实现的,所以我们替换mallocfree后,new和delete也会被记录。但是,为了确保覆盖,我们也可以重载new和delete操作符(但这需要在C++代码中显式重载,无法通过LD_PRELOAD实现,因为它们是C++符号,名字修饰不同)。因此,对于C++程序,可能需要同时替换malloc/free和重载new/delete。 ### 步骤6:检查动态库加载情况 使用`ldd`命令检查程序依赖的动态库,确认我们的动态库被正确预加载。 ### 步骤7:使用工具追踪 如果上述方法仍然无效,可以使用系统工具如`strace`或`ltrace`来跟踪系统调用和库函数调用,以确定程序实际调用了哪些函数。 例如,使用ltrace: ```bash ltrace -e malloc,free,calloc,realloc ./your_program ``` ### 总结 动态库替换malloc/calloc无法捕捉调用的问题,通常是由于动态库没有覆盖所有内存分配函数,或者程序使用了静态链接、其他分配库等原因。通过扩展动态库覆盖更多函数,并确保正确使用LD_PRELOAD,可以解决大部分问题。 如果问题仍然存在,可能需要考虑程序本身是否绕过了标准库的内存分配函数,或者使用了自定义的内存分配器。 ### 相关问题 1. 如何通过LD_PRELOAD替换动态库中的其他函数? 2. 在C++中,如何替换全局的new和delete操作符? 3. 使用LD_PRELOAD时,如何避免与其他预加载库的冲突? 4. 除了LD_PRELOAD,还有哪些方法可以钩住(hook)内存分配函数?</think>### 动态库替换 malloc/calloc 无法捕捉调用的问题分析 #### 问题原因 当使用动态库方式替换标准库的 `malloc`/`calloc` 时,无法捕捉调用通常由以下原因导致: 1. **符号覆盖失效** - 动态库需通过 `LD_PRELOAD` 预加载才能覆盖标准库符号[^1]。若未正确设置环境变量(如 `export LD_PRELOAD=/path/to/your_lib.so`),替换将失效。 - 程序可能静态链接了内存分配函数,导致动态库无法覆盖静态符号。 2. **函数签名不匹配** - 替换函数必须严格匹配标准库签名: ```c void* malloc(size_t size); void* calloc(size_t num, size_t size); ``` 若自定义函数的参数类型或返回类型不匹配(如使用 `int` 代替 `size_t`),链接器不会将其视为有效覆盖。 3. **间接调用问题** - C++ 程序可能通过 `operator new` 分配内存,其底层调用 `malloc`,但若重载了 `operator new` 而未指向自定义 `malloc`,调用仍指向原函数。 - 第三方库(如 OpenSSL)可能使用内部内存分配器,绕过标准库函数。 4. **线程局部存储(TLS)干扰** - 某些分配器(如 tcmalloc)使用线程局部存储加速访问。若替换库未处理 TLS,可能导致原始函数被调用。 --- #### 解决方案 ##### 步骤1:验证动态库加载 确保正确预加载动态库: ```bash # 临时生效 LD_PRELOAD=/path/to/your_malloc.so ./your_program # 永久生效(谨慎使用) export LD_PRELOAD=/path/to/your_malloc.so ./your_program ``` 使用 `ldd` 检查依赖: ```bash ldd your_program | grep malloc # 应显示 your_malloc.so ``` ##### 步骤2:确保函数签名正确 自定义动态库示例: ```c // your_malloc.c #include <dlfcn.h> #include <stdio.h> void* malloc(size_t size) { static void* (*real_malloc)(size_t) = NULL; if (!real_malloc) { real_malloc = dlsym(RTLD_NEXT, "malloc"); // 获取标准库函数 } void* ptr = real_malloc(size); printf("malloc(%zu) = %p\n", size, ptr); // 捕捉调用 return ptr; } ``` 编译为动态库: ```bash gcc -shared -fPIC -o your_malloc.so your_malloc.c -ldl ``` ##### 步骤3:处理 C++ 和第三方库 - **C++ 程序**:需额外覆盖 `operator new`: ```c++ void* operator new(size_t size) { return your_custom_malloc(size); } ``` - **第三方库**:通过 `dlsym` 强制重定向: ```c void* (*original_openssl_malloc)(size_t) = NULL; void* custom_openssl_malloc(size_t size) { if (!original_openssl_malloc) { original_openssl_malloc = dlsym(RTLD_NEXT, "CRYPTO_malloc"); } return your_custom_malloc(size); // 指向你的实现 } ``` ##### 步骤4:验证内存分配失败处理 引用[2]强调必须检查分配结果: ```c void* ptr = malloc(1e9); // 大内存可能失败 if (ptr == NULL) { // 必须检查空指针! perror("malloc failed"); exit(EXIT_FAILURE); } ``` 在替换函数中同样需实现返回值检查逻辑。 --- #### 调试技巧 1. **使用 `strace` 跟踪系统调用**: ```bash strace -e brk,mmap,mprotect ./your_program ``` 2. **通过 GDB 拦截调用**: ```gdb break __libc_malloc run backtrace ``` 3. **日志增强**:在替换函数中添加调用栈打印: ```c #include <execinfo.h> void print_stack() { void* buffer[10]; backtrace(buffer, 10); backtrace_symbols_fd(buffer, 10, STDOUT_FILENO); } ``` --- ### 相关问题 1. **如何确保自定义内存分配器的线程安全性?** - 需实现锁机制或使用原子操作,避免多线程竞争。 2. **`LD_PRELOAD` 失效的常见场景有哪些?** - SUID 程序(权限隔离)、静态链接二进制、容器环境(如 Docker 需额外配置)。 3. **替换 `realloc` 时如何避免内存泄漏?** - 必须正确处理旧指针的释放和新内存块的迁移逻辑。 4. **动态库替换对程序性能的影响如何评估?** - 可通过 `perf` 工具对比替换前后的指令周期和缓存命中率。 [^1]: 动态内存分配函数的区别与使用要点 [^2]: 未检查内存分配返回值的风险及后果
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值