在C语言中创建malloc和free的包装函数

45

我正在尝试为C语言中的freemalloc创建包装函数,以帮助我检测内存泄漏。有人知道如何声明这些函数,以便在调用malloc()free()时调用我的自定义函数而不是标准库函数吗?


2
顺便提一下,像Valgrind这样的工具就是做这个的。如果你宁愿在Unix或Linux上使用开箱即用的东西,那么Valgrind是一个不错的选择。 - sudo
相关:什么是LD_PRELOAD技巧? - Gabriel Staples
10个回答

85

有几种选择:

  1. 针对GLIBC (大多数Linux) 的解决方案。 如果您的编译环境是使用 gccglibc,则首选方法是使用malloc hooks(malloc 钩子)。它不仅可以让您指定自定义的 mallocfree,而且还会通过堆栈上的返回地址标识调用者。

  2. 针对POSIX的解决方案。mallocfree 定义为可执行文件中原始分配例程的包装器,这将“覆盖”来自 libc 的版本。在包装器中,您可以调用原始的 malloc 实现,您可以使用 dlsymRTLD_NEXT 句柄查找。定义包装器函数的应用程序或库需要链接到 -ldl

  3. #define _GNU_SOURCE
    #include <dlfcn.h>
    #include <stdio.h>
    
    void* malloc(size_t sz)
    {
        void *(*libc_malloc)(size_t) = dlsym(RTLD_NEXT, "malloc");
        printf("malloc\n");
        return libc_malloc(sz);
    }
    
    void free(void *p)
    {
        void (*libc_free)(void*) = dlsym(RTLD_NEXT, "free");
        printf("free\n");
        libc_free(p);
    }
    
    int main()
    {
        free(malloc(10));
        return 0;
    }
    
  4. 仅适用于Linux系统。 您可以通过在LD_PRELOAD环境变量中指定函数来非侵入式地覆盖动态库中的函数。

  5. LD_PRELOAD=mymalloc.so ./exe
    
  6. 仅适用于Mac OSX。

    与Linux相同,不同之处在于您将使用DYLD_INSERT_LIBRARIES环境变量。


1
选项2在正常运行时表现良好,但当使用valgrind运行应用程序时,会出现各种奇怪的问题。这是因为valgrind执行了类似的覆盖并引起了某种冲突吗?哪个选项最适合与valgrind一起使用以包装malloc? - davidA
3
你正在自定义的malloc内使用printf(),但printf()本身又使用malloc。例如LD_PRELOAD=./malloc.so ls会导致段错误。这不是创建无限递归吗?如何告诉我们自定义的malloc内部的函数使用libc-malloc? - phip1611
你的posix示例现在会导致一个大的段错误。 - peterh
另外,还可以参考这个教程来构建 *.so 共享对象:https://www.cprogramming.com/tutorial/shared-libraries-linux-gcc.html。 - Gabriel Staples
1
如果您重写 malloc()free(),而不是仅仅包装它们,则必须考虑一些特殊情况,例如,如果您从 malloc() 中调用 printf(),可能会创建无限递归,因为 printf() 可能会调用malloc(),从而又调用printf()...直到堆栈溢出。请参见我在这里的回答:“Segmentation fault (core dumped)” for: “No such file or directory” for libioP.h, printf-parse.h, vfprintf-internal.c, etc - Gabriel Staples
显示剩余6条评论

20
可以使用LD_PRELOAD创建一个包装器和“覆盖”函数,类似于之前展示的示例。
LD_PRELOAD=/path.../lib_fake_malloc.so ./app

但我建议以“稍微”聪明的方式来做,我的意思是只调用一次dlsym

#define _GNU_SOURCE
#include <stdio.h>
#include <stdint.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 *p = real_malloc(size);
    fprintf(stderr, "malloc(%d) = %p\n", size, p);
    return p;
}

我在这里找到的示例:http://www.jayconrod.com/cgi/view_post.py?23,是 Jay Conrod 的帖子。

但是我在这个页面上发现真正酷的东西是:GNU 链接器提供了一个有用的选项,--wrap。当我查看“man ld”时,出现以下示例:

void *
__wrap_malloc (size_t c)
{
    printf ("malloc called with %zu\n", c);
    return __real_malloc (c);
}

我同意他们所说的“平凡示例” :). 即使没有dlsym,也不需要它。

让我引用我“man ld”页面的另一部分:

--wrap=symbol
       Use a wrapper function for symbol.
       Any undefined reference to symbol will be resolved to "__wrap_symbol".
       Any undefined reference to "__real_symbol" will be resolved to symbol.
我希望说明已经完整,并展示了如何使用这些东西。

1
嗨,我遇到了错误,该如何解决?错误信息:ld.so: object '/home/tmp/libjmalloc.so' from LD_PRELOAD cannot be preloaded: ignored. - Thangaraj
1
我在两个系统中遇到了问题,其中一个系统我通过将相对路径替换为绝对路径来进行了更正,而在另一个系统中仍在挖掘 : )。我有一个疑问,这种方法对于小程序是有效的。假设我有一个大型程序,那么我该如何找出malloc函数是从哪个函数调用的呢? - Thangaraj
你有什么想法吗?我有一个疑问,这种方法对于小程序来说是可行的。如果我有一个大型程序,那么我该如何找出调用 malloc 函数的哪个函数? - Thangaraj
@thangaraj 如果你想知道调用方是谁,我建议你将方法更改或与以下方法结合使用:二进制插装像这里或跟踪/调试,如gdb,或基于ptrace的sydbox或pinktrace - 这里有关于跟踪内存分配的类似想法。在malloc中使用gdb设置断点并检查回溯听起来很合理。 - Grzegorz Wierzowiecki
--wrap 链接器标志如何与 glibc 中的 malloc 调用配合工作?即,它是否也会将 strdup 内部的 malloc 重命名为包装器? - Dan M.
显示剩余2条评论

14

在我的情况下,我需要在malloc下封装memalign/aligned_malloc。在尝试其他解决方案后,最终实现了下面列出的方法。它似乎可以正常工作。

mymalloc.c

/* 
 * Link-time interposition of malloc and free using the static
 * linker's (ld) "--wrap symbol" flag.
 * 
 * Compile the executable using "-Wl,--wrap,malloc -Wl,--wrap,free".
 * This tells the linker to resolve references to malloc as
 * __wrap_malloc, free as __wrap_free, __real_malloc as malloc, and
 * __real_free as free.
 */
#include <stdio.h>

void *__real_malloc(size_t size);
void __real_free(void *ptr);


/* 
 * __wrap_malloc - malloc wrapper function 
 */
void *__wrap_malloc(size_t size)
{
    void *ptr = __real_malloc(size);
    printf("malloc(%d) = %p\n", size, ptr);
    return ptr;
}

/* 
 * __wrap_free - free wrapper function 
 */
void __wrap_free(void *ptr)
{
    __real_free(ptr);
    printf("free(%p)\n", ptr);
}
 

5
这是我多年来使用的一组封装函数(当我需要用C语言时仍然使用),用于检测未释放的内存、多次释放的内存、对已释放内存的引用、缓冲区溢出/下溢以及释放未分配的内存。
你可以使用宏预处理器将malloc和free重新定义为mem包中的函数,但我建议不要这样做,因为它不会像strdup那样重定向库调用到malloc。
这些函数已经存在了25年,并且已经证明了它们的可靠性。 ftp://ftp.digitalmars.com/ctools.zip

受身份验证保护的链接 - Christoffer Bubach

5
在C中,我使用的方法类似于:
#define malloc(x) _my_malloc(x, __FILE__, __LINE__)
#define free(x) _my_free(x)

这使我能够比较容易地检测到内存分配的行和文件,应该是跨平台的,但如果宏已经定义(只有在使用另一个内存泄漏检测器时才会出现这种情况),则会遇到问题。
如果你想在C++中实现相同的功能,过程会稍微复杂一些,但使用相同的技巧。

1
最好不要在名称中使用前导下划线 - 它们主要保留给实现。 - Jonathan Leffler
@JonathanLeffler,以__和下划线加大写字母开头的名称是由标准保留的。以单个下划线开头的名称没有被保留,但它们应该包含在文件内,即链接器不应该看到它们。因此,只要_my_malloc_my_free是静态函数,就可以使用它们。另一方面,重新定义库函数是未定义行为。 - Shahbaz
2
@Shahbaz: ISO/IEC 9899:2011,§7.1.3 保留标识符 表示:— 所有以下划线和大写字母或另一个下划线开头的标识符始终保留用于任何用途。所有以下划线开头的标识符始终保留用于普通名称空间和标记名称空间中具有文件范围的标识符。 我认为 _my_malloc() 作为静态函数违反了第二个限制。 - Jonathan Leffler
@Shahbaz:你的评论“_static void *_my_malloc(size_t)在文件作用域中定义,属于普通名称空间”是否与标准说法“所有以下划线开头的标识符都保留为普通名称空间中具有文件作用域的标识符”相冲突?我错过了什么吗? - Jonathan Leffler
@JonathanLeffler,我曾将“文件作用域保留”解释为只要在文件作用域内就可以使用,即保留给程序员使用(只要在文件作用域内)。也许你是对的,但如果你看一下GNU关于保留名称的描述,其中说到(重点在于):保留名称包括所有以下划线(“_”)开头的外部标识符(全局函数和变量)... 更符合我的理解(虽然他们可能是错的)。 - Shahbaz
显示剩余3条评论

4

如果你的目标是消除内存泄漏问题,一种更加简单、不会干扰程序的方法是使用类似于Valgrind(免费)或者Purify(收费)这样的工具。


1
同意。Valgrind非常棒。我喜欢它如何与编译后的二进制文件一起工作。你不需要特别编译它或做任何事情,尽管如果你使用-O0和-g标志编译,你会得到最好的结果。 - eric.frederich

2
如果您为malloc()和free()定义了自己的函数,并将其与应用程序显式链接,则应优先使用您的函数而不是库中的函数。
但是,您称之为“malloc”的函数不能调用库malloc函数,因为在“c”中没有单独的命名空间概念。换句话说,您必须自己实现malloc和free的内部。
另一种方法是编写my_malloc()和my_free()函数,它们调用标准库函数。这意味着调用malloc的任何代码都必须更改为调用您的my_xxx函数。

1
你可以使用 #define malloc my_malloc 来使你的代码在不修改的情况下正常工作。但是你需要保持一致的使用方式 - 如果内存将在库中释放,就不要使用 my_malloc,反之亦然。 - Mark Ransom
第二段和第三段是误导性的。 - Matt Joiner
@Matt Joiner - 你能详细说明一下吗? - Roddy
P2: 如果有名称空间也不会改变现状。而且您可以随后调用真正的malloc函数。这与C语言无关。P3:是的,您可以这样做,并且这不会正确地挂钩对malloc / free的调用,而无需控制该代码。您可以指示链接器重定向到不同名称的引用。而且这一切都没有修改C代码。总结:您声称的限制实际上并不存在,并且您提供的解决方法都是不必要的。 - Matt Joiner
@Matt - 谢谢:我不知道malloc钩子和--wrap,但它们高度依赖于工具链和操作系统。据我所知,只有gcc支持它们,而且OP没有指定操作系统或工具。C++命名空间可以提供类似于#define方法的解决方案,但我同意它远非理想,也存在你提到的问题。总的来说,我对我的答案仍然很满意。 - Roddy

1
如果您是自定义mallocfree的唯一客户端(即您不尝试为某个其他库中的代码打补丁),则可以使用依赖注入。
#ifndef ALLOCATOR_H
#define ALLOCATOR_H

#include <stddef.h>

struct Allocator;

typedef struct {
    void *(*allocate)(struct Allocator *allocator, size_t size);

    void (*free)(struct Allocator *allocator, void *object);
} AllocatorVTable;

typedef struct Allocator {
    const AllocatorVTable *vptr;
} Allocator;

typedef struct {
    Allocator super;
    char *buffer;
    size_t offset;
    size_t capacity;
} BufferedAllocator;

void BufferedAllocator_init(BufferedAllocator *allocator, char *buffer, size_t capacity);

typedef Allocator MallocAllocator;

void MallocAllocator_init(MallocAllocator *allocator);

void *Allocator_allocate(Allocator *allocator, size_t size);

void Allocator_free(Allocator *allocator, void *object);

#endif

#include "allocator.h"
#include "malloc.h"

void *Allocator_allocate(Allocator *allocator, size_t size) {
    return allocator->vptr->allocate(allocator, size);
}

void Allocator_free(Allocator *allocator, void *object) {
    allocator->vptr->free(allocator, object);
}

void *BufferedAllocator_allocate(Allocator *allocator, size_t size) {
    BufferedAllocator *bufferedAllocator = (BufferedAllocator *) allocator;
    if (bufferedAllocator->offset + size > bufferedAllocator->capacity) {
        fprintf(stderr, "buffer overflow: %ld + %ld > %ld\n",
                bufferedAllocator->offset, size, bufferedAllocator->capacity);
        return NULL;
    }
    bufferedAllocator->offset += size;
    return bufferedAllocator->buffer + bufferedAllocator->offset - size;
}

void BufferedAllocator_free(Allocator *allocator, void *object) {

}

const AllocatorVTable bufferedAllocatorVTable = {
        .allocate = BufferedAllocator_allocate,
        .free = BufferedAllocator_free,
};

void BufferedAllocator_init(BufferedAllocator *allocator, char *buffer,
                            size_t capacity) {
    allocator->super.vptr = &bufferedAllocatorVTable;
    allocator->buffer = buffer;
    allocator->offset = 0;
    allocator->capacity = capacity;
}

void *MallocAllocator_allocate(Allocator *allocator, size_t size) {
    return malloc(size);
}

void MallocAllocator_free(Allocator *allocator, void *object) {
    free(object);
}

const AllocatorVTable mallocAllocatorVTable = {
        .allocate = MallocAllocator_allocate,
        .free = MallocAllocator_free,
};

void MallocAllocator_init(MallocAllocator *allocator) {
    allocator->vptr = &mallocAllocatorVTable;
}

#include <assert.h>
#include "allocator_test.h"
#include "allocator.h"

void testAllocator() {
    {
        BufferedAllocator bufferedAllocator;
        char buffer[4];
        size_t capacity = sizeof(buffer);
        BufferedAllocator_init(&bufferedAllocator, buffer, capacity);
        Allocator *allocator = &bufferedAllocator.super;

        void *chill = Allocator_allocate(allocator, capacity);
        assert(chill == buffer);
        void *oops = Allocator_allocate(allocator, 1);
        assert(oops == NULL);
    }

    {
        MallocAllocator allocator;
        MallocAllocator_init(&allocator);

        void *chill = Allocator_allocate(&allocator, 100);
        assert(chill != NULL);
        void *alsoChill = Allocator_allocate(&allocator, 100);
        assert(alsoChill != NULL);
    }
}

因此,您需要向您想要分配东西的任何代码传递一个 Allocator *(除了在栈上使用char buf[n]之类的东西)。 您可以使用 MallocAllocator 来仅使用系统的 malloc/free,或者您可以在程序的顶部使用 BufferedAllocatorBufferedAllocator 只是一个非常简单的 malloc/free 的示例。 在我的使用案例中,它运行良好,因为我基本上知道我的程序将预先使用多少内存,并且在整个程序完成之前不删除任何对象。 使用此接口,您可以编写更复杂的算法,例如this lecture 中描述的算法之一。 有许多不同的防止碎片化的策略和许多权衡,因此自己编写 malloc/free 可能非常有用。

喜欢你的稳健方法,将使用 i_p_c 命名约定来实现。 - Alan Turing

0
如果您正在使用Linux,您可以使用malloc_hook()(使用GNU glibc)。此函数允许您在调用实际的malloc之前调用malloc_hook()函数。手册中有一个示例说明如何使用它。

0

如果你只是谈论你自己控制的内存,即你自己分配和释放的内存,你可以看一下rmdebug。可能这正是你要写的,所以你可以节省时间。它有一个非常自由的许可证,如果这对你很重要的话。

我个人在一个项目中使用它来查找内存泄漏,好处是它比valgrind快得多,但它不是那么强大,所以你不能得到完整的调用堆栈。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接