使用LD_PRELOAD机制覆盖'malloc'函数

50

我试图编写一个简单的共享库,将malloc调用记录到stderr(类似于'mtrace')。

然而,这并不起作用。 以下是我的操作:

/* mtrace.c */
#include <dlfcn.h>
#include <stdio.h>

static void* (*real_malloc)(size_t);

void *malloc(size_t size)
{
    void *p = NULL;
    fprintf(stderr, "malloc(%d) = ", size);
    p = real_malloc(size);
    fprintf(stderr, "%p\n", p);
    return p;
}

static void __mtrace_init(void) __attribute__((constructor));
static void __mtrace_init(void)
{
    void *handle = NULL;
    handle = dlopen("libc.so.6", RTLD_LAZY);
    if (NULL == handle) {
        fprintf(stderr, "Error in `dlopen`: %s\n", dlerror());
        return;
    }
    real_malloc = dlsym(handle, "malloc");
    if (NULL == real_malloc) {
        fprintf(stderr, "Error in `dlsym`: %s\n", dlerror());
        return;
    }
}

我使用以下命令进行编译:

gcc -shared -fPIC -o mtrace.so mtrace.c

然后当我尝试执行ls时:

$ LD_PRELOAD=./mtrace.so ls
malloc(352) = Segmentation fault

我怀疑dlopen需要malloc,而且由于我在共享库中重新定义了它,因此它使用的是仍未分配real_malloc版本。

问题是...我该如何使其工作?

P.S.对于标签的匮乏,我很抱歉,我找不到合适的标签,而且我还没有足够的声望来创建新的标签。


我有同样的问题。看起来构造函数并不总是被调用。 - Dhaval Kapil
对我来说,问题在于 printf() 不能与重载的 malloc 一起使用,但是 fprintf() 可以。 - Hi-Angel
5个回答

52

我总是这样做:

#define _GNU_SOURCE

#include <stdio.h>
#include <dlfcn.h>

static void* (*real_malloc)(size_t)=NULL;

static void mtrace_init(void)
{
    real_malloc = dlsym(RTLD_NEXT, "malloc");
    if (NULL == real_malloc) {
        fprintf(stderr, "Error in `dlsym`: %s\n", dlerror());
    }
}

void *malloc(size_t size)
{
    if(real_malloc==NULL) {
        mtrace_init();
    }

    void *p = NULL;
    fprintf(stderr, "malloc(%d) = ", size);
    p = real_malloc(size);
    fprintf(stderr, "%p\n", p);
    return p;
}

不要使用构造函数,只需在第一次调用 malloc 时进行初始化。使用 RTLD_NEXT 避免使用 dlopen。你还可以尝试使用malloc hooks。请注意,所有这些都是 GNU 扩展,可能在其他地方无法正常工作。


为什么重要的是只调用一次dlsym(RTLD_NEXT, "malloc")函数? - Hugo
@Hugo 这并不是必需的。您可以在每次调用时查找malloc函数。它只会慢一点。 - Piotr Praszmo
这个例子支持在.so对象中使用调用malloc的函数吗?例如,我尝试在您的代码中添加时间打印(使用gettimeofday、strftime),但代码无法工作(被卡住了)。 - Yos
6
不使用构造函数,在第一次调用 malloc 时进行初始化。这样做会导致代码不支持多线程。当前发布的代码还隐含地假设 fprintf() 永远不会调用 malloc() 。如果 fprintf() 使用了 malloc(),那么这段代码将会陷入无限递归。 - Andrew Henle
2
虽然这段代码可以覆盖单个malloc()函数,但如果我们以类似的方式覆盖calloc()函数,则会失败。这个答案不应该被接受,因为它不是一个合适的解决方案。 - pulse
最好能够尽可能使用__attribute__((constructor)),而不是在每次调用包装器时检查正确的初始化,这样做不是更好吗? - Phoenix87

39
如果您确实想要在malloc中使用LD_PRELOAD,并发现已接受答案中的代码仍然会导致段错误,我有一个似乎有效的解决方案。段错误是由dlsym调用calloc分配了32个字节造成栈递归到最后引起的。
我的解决方案是创建一个超级简单的静态分配器,在dlsym返回malloc函数指针之前处理分配问题。
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>

char tmpbuff[1024];
unsigned long tmppos = 0;
unsigned long tmpallocs = 0;

void *memset(void*,int,size_t);
void *memmove(void *to, const void *from, size_t size);

/*=========================================================
 * interception points
 */

static void * (*myfn_calloc)(size_t nmemb, size_t size);
static void * (*myfn_malloc)(size_t size);
static void   (*myfn_free)(void *ptr);
static void * (*myfn_realloc)(void *ptr, size_t size);
static void * (*myfn_memalign)(size_t blocksize, size_t bytes);

static void init()
{
    myfn_malloc     = dlsym(RTLD_NEXT, "malloc");
    myfn_free       = dlsym(RTLD_NEXT, "free");
    myfn_calloc     = dlsym(RTLD_NEXT, "calloc");
    myfn_realloc    = dlsym(RTLD_NEXT, "realloc");
    myfn_memalign   = dlsym(RTLD_NEXT, "memalign");

    if (!myfn_malloc || !myfn_free || !myfn_calloc || !myfn_realloc || !myfn_memalign)
    {
        fprintf(stderr, "Error in `dlsym`: %s\n", dlerror());
        exit(1);
    }
}

void *malloc(size_t size)
{
    static int initializing = 0;
    if (myfn_malloc == NULL)
    {
        if (!initializing)
        {
            initializing = 1;
            init();
            initializing = 0;

            fprintf(stdout, "jcheck: allocated %lu bytes of temp memory in %lu chunks during initialization\n", tmppos, tmpallocs);
        }
        else
        {
            if (tmppos + size < sizeof(tmpbuff))
            {
                void *retptr = tmpbuff + tmppos;
                tmppos += size;
                ++tmpallocs;
                return retptr;
            }
            else
            {
                fprintf(stdout, "jcheck: too much memory requested during initialisation - increase tmpbuff size\n");
                exit(1);
            }
        }
    }

    void *ptr = myfn_malloc(size);
    return ptr;
}

void free(void *ptr)
{
    // something wrong if we call free before one of the allocators!
//  if (myfn_malloc == NULL)
//      init();

    if (ptr >= (void*) tmpbuff && ptr <= (void*)(tmpbuff + tmppos))
        fprintf(stdout, "freeing temp memory\n");
    else
        myfn_free(ptr);
}

void *realloc(void *ptr, size_t size)
{
    if (myfn_malloc == NULL)
    {
        void *nptr = malloc(size);
        if (nptr && ptr)
        {
            memmove(nptr, ptr, size);
            free(ptr);
        }
        return nptr;
    }

    void *nptr = myfn_realloc(ptr, size);
    return nptr;
}

void *calloc(size_t nmemb, size_t size)
{
    if (myfn_malloc == NULL)
    {
        void *ptr = malloc(nmemb*size);
        if (ptr)
            memset(ptr, 0, nmemb*size);
        return ptr;
    }

    void *ptr = myfn_calloc(nmemb, size);
    return ptr;
}

void *memalign(size_t blocksize, size_t bytes)
{
    void *ptr = myfn_memalign(blocksize, bytes);
    return ptr;
}

希望这能帮到某人。


4
肯定做到了。请查看 https://github.com/jtolds/malloc_instrumentation/blob/9f8387bb270dd194e57aca38dc45039f4e54cc0a/malloc_instrument.c 上的第一条评论,尽管我应该指出我们后来找到了更好的方法 https://github.com/jtolds/malloc_instrumentation/commit/731c590ae88fcaa4d134ed0fc6b7b8f2836c520b。 - jtolds
3
如果您使用glibc,可以使用__libc_calloc而不是制作静态分配器。@bdonlan的回答提到了glibc支持的更好的方法,但我想尝试dlsym。这里有一个示例:https://github.com/arhuaco/ram-is-mine/blob/master/src/ram_is_mine.c。检查void *realloc(...)。 - arhuaco
@jtolds使用__sync_fetch_and_add__sync_fetch_and_sub的原因是什么?是为了设置一些内存屏障吗? - dashesy
如果调用real free来释放临时缓冲区中的内存,程序会崩溃吗? - hitchhiker
从临时缓冲区中释放不会产生影响,但不应该崩溃。 - FatalFlaw

8
如果您正在使用glibc,应使用其内置的malloc hooking机制 - 本页面中的示例有一个查找原始malloc的示例。如果您正在添加附加跟踪信息以进行分配,则特别重要,以确保返回malloc'd缓冲区的库函数与您的free()实现一致。

5
但是malloc钩子现已被弃用。 - dashesy
@dashesy: 从什么时候开始的?最新版本的文档中没有提到它们被弃用了。 - Daniel Kamil Kozar
2
这里是@DanielKamilKozar的一个参考链接:https://dev59.com/iGMm5IYBdhLWcg3wMssp。 - dashesy
同样,https://developers.redhat.com/articles/2021/08/25/securing-malloc-glibc-why-malloc-hooks-had-go - Vladimir F Героям слава

5

以下是对上述示例的扩展,它通过使用mmap在初始化完成之前避免了dlsym中的段错误:

#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <sys/mman.h>

static void* (*real_malloc)(size_t)         = NULL;
static void* (*real_realloc)(void*, size_t) = NULL;
static void* (*real_calloc)(size_t, size_t) = NULL;
static void  (*real_free)(void*)            = NULL;

static int alloc_init_pending = 0;

/* Load original allocation routines at first use */
static void alloc_init(void)
{
  alloc_init_pending = 1;
  real_malloc  = dlsym(RTLD_NEXT, "malloc");
  real_realloc = dlsym(RTLD_NEXT, "realloc");
  real_calloc  = dlsym(RTLD_NEXT, "calloc");
  real_free    = dlsym(RTLD_NEXT, "free");
  if (!real_malloc || !real_realloc || !real_calloc || !real_free) {
    fputs("alloc.so: Unable to hook allocation!\n", stderr);
    fputs(dlerror(), stderr);
    exit(1);
  } else {
    fputs("alloc.so: Successfully hooked\n", stderr);
  }
  alloc_init_pending = 0;
}

#define ZALLOC_MAX 1024
static void* zalloc_list[ZALLOC_MAX];
static size_t zalloc_cnt = 0;

/* dlsym needs dynamic memory before we can resolve the real memory 
 * allocator routines. To support this, we offer simple mmap-based 
 * allocation during alloc_init_pending. 
 * We support a max. of ZALLOC_MAX allocations.
 * 
 * On the tested Ubuntu 16.04 with glibc-2.23, this happens only once.
 */
void* zalloc_internal(size_t size)
{
  fputs("alloc.so: zalloc_internal called", stderr);
  if (zalloc_cnt >= ZALLOC_MAX-1) {
    fputs("alloc.so: Out of internal memory\n", stderr);
    return NULL;
  }
  /* Anonymous mapping ensures that pages are zero'd */
  void* ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0);
  if (MAP_FAILED == ptr) {
    perror("alloc.so: zalloc_internal mmap failed");
    return NULL;
  }
  zalloc_list[zalloc_cnt++] = ptr; /* keep track for later calls to free */
  return ptr;
}

void free(void* ptr)
{
  if (alloc_init_pending) {
    fputs("alloc.so: free internal\n", stderr);
    /* Ignore 'free' during initialization and ignore potential mem leaks 
     * On the tested system, this did not happen
     */
    return;
  }
  if(!real_malloc) {
    alloc_init();
  }
  for (size_t i = 0; i < zalloc_cnt; i++) {
    if (zalloc_list[i] == ptr) {
      /* If dlsym cleans up its dynamic memory allocated with zalloc_internal,
       * we intercept and ignore it, as well as the resulting mem leaks.
       * On the tested system, this did not happen
       */
      return;
    }
  }
  real_free(ptr);
}

void *malloc(size_t size)
{
  if (alloc_init_pending) {
    fputs("alloc.so: malloc internal\n", stderr);
    return zalloc_internal(size);
  }
  if(!real_malloc) {
    alloc_init();
  }
  void* result = real_malloc(size);
  //fprintf(stderr, "alloc.so: malloc(0x%zx) = %p\n", size, result);
  return result;
}

void *realloc(void* ptr, size_t size)
{
  if (alloc_init_pending) {
    fputs("alloc.so: realloc internal\n", stderr);
    if (ptr) {
      fputs("alloc.so: realloc resizing not supported\n", stderr);
      exit(1);
    }
    return zalloc_internal(size);
  }
  if(!real_malloc) {
    alloc_init();
  }
  return real_realloc(ptr, size);
}

void *calloc(size_t nmemb, size_t size)
{
  if (alloc_init_pending) {
    fputs("alloc.so: calloc internal\n", stderr);
    /* Be aware of integer overflow in nmemb*size.
     * Can only be triggered by dlsym */
    return zalloc_internal(nmemb * size);
  }
  if(!real_malloc) {
    alloc_init();
  }
  return real_calloc(nmemb, size);
}

4

这是关于malloc和free hooking的最简单示例。

#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>

static void* (*real_malloc)(size_t size);
static void  (*real_free)(void *ptr);

__attribute__((constructor))
static void init()
{
        real_malloc = dlsym(RTLD_NEXT, "malloc");
        real_free   = dlsym(RTLD_NEXT, "free");
        fprintf(stderr, "init\n");
}

void *malloc(size_t size)
{
        void *ptr = real_malloc(size);
        fprintf(stderr, "malloc(%zd) = %p\n", size, ptr);
        return ptr;
}

void free(void *ptr)
{
        real_free(ptr);
        fprintf(stderr, "free(%p)\n", ptr);
}

你会如何使用它?将这段代码放在 main 前面并调用 init 函数吗? - HolKann
1
虽然现在已经很晚了,但是这个程序可以编译成一个共享库,然后在运行目标程序时使用LD_PRELOAD进行预加载。 - Honggyu Kim

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