如何正确插入允许 LD_PRELOAD 链接的 malloc

How to correctly interpose malloc allowing for LD_PRELOAD chaining

我创建了一个共享库,其中插入了 malloc() 和相关调用。 效果很好,但有一些警告。 有一件事是行不通的。我希望能够链接插入器,这样我就可以 运行 类似

LD_PRELOAD="/path/to/mymalloc.so /usr/lib64/jemalloc.so" some_app

目的是我的库现在应该通过 RTLD_NEXT.

转发到 jemalloc,而不是转发到 libc malloc()

但是它会生成堆栈跟踪段错误,显示我的 malloc 包装器无限期地调用自己。尽管在不使用 jemalloc 时它自己不分配任何内存:

#224364 0x00007facb1aef46a in Memory::HybridAllocator<Memory::LibCAllocator, Memory::StaticAllocator>::malloc (this=0x7facb1d0be60 <Memory::getHybridAllocator()::hybrid>, size=72704) at /home/brucea/work/git/libbede/src/main/cpp/memory/Memory/HybridAllocator.h:109
#224365 0x00007facb1aefa8a in malloc (size=72704) at /home/brucea/work/git/libbede/src/main/cpp/memory/Memory/mallocwrap.cpp:11
#224366 0x00007facb1aeeca2 in Memory::LibCAllocator::malloc (this=0x7facb1cf3720 <Memory::getBootstrapAllocator()::bootstrap>, requestSize=72704) at /home/brucea/work/git/libbede/src/main/cpp/memory/Memory/LibCAllocator.h:77
#224367 0x00007facb1aef46a in Memory::HybridAllocator<Memory::LibCAllocator, Memory::StaticAllocator>::malloc (this=0x7facb1d0be60 <Memory::getHybridAllocator()::hybrid>, size=72704) at /home/brucea/work/git/libbede/src/main/cpp/memory/Memory/HybridAllocator.h:109
#224368 0x00007facb1aefa8a in malloc (size=72704) at /home/brucea/work/git/libbede/src/main/cpp/memory/Memory/mallocwrap.cpp:11
#224369 0x00007facb133fc1a in (anonymous namespace)::pool::pool (this=0x7facb163e200 <(anonymous namespace)::emergency_pool>) at ../../../../libstdc++-v3/libsupc++/eh_alloc.cc:123

#224370 __static_initialization_and_destruction_0 (__priority=65535, __initialize_p=1) at ../../../../libstdc++-v3/libsupc++/eh_alloc.cc:262
#224371 _GLOBAL__sub_I_eh_alloc.cc(void) () at ../../../../libstdc++-v3/libsupc++/eh_alloc.cc:338
#224372 0x00007facb1d1b8ba in call_init (l=<optimized out>, argc=argc@entry=4, argv=argv@entry=0x7ffe3ba440e8, env=env@entry=0x7ffe3ba44110) at dl-init.c:72
#224373 0x00007facb1d1b9ba in call_init (env=0x7ffe3ba44110, argv=0x7ffe3ba440e8, argc=4, l=<optimized out>) at dl-init.c:30
#224374 _dl_init (main_map=0x7facb1f3a1d0, argc=4, argv=0x7ffe3ba440e8, env=0x7ffe3ba44110) at dl-init.c:119
#224375 0x00007facb1d0cfda in _dl_start_user () from /lib64/ld-linux-x86-64.so.2
#224376 0x0000000000000004 in ?? ()
#224377 0x00007ffe3ba45d7f in ?? ()
#224378 0x00007ffe3ba45ddd in ?? ()
#224379 0x00007ffe3ba45de0 in ?? ()
#224380 0x00007ffe3ba45de4 in ?? ()
#224381 0x0000000000000000 in ?? ()

在 gdb 中调试,原因似乎是 __libc_malloc() 中的 malloc_hook 以某种方式设置为指向我的 malloc 实现,导致无限递归。但这一定是 jemalloc 以某种方式做的。

__GI___libc_malloc (bytes=16) at malloc.c:3037
3037    {
(gdb) s
3042        = atomic_forced_read (__malloc_hook);
(gdb) s
3043      if (__builtin_expect (hook != NULL, 0))
(gdb) s
3044        return (*hook)(bytes, RETURN_ADDRESS (0));
(gdb) s
malloc (size=140737488345424) at /home/brucea/work/git/libbede/src/main/cpp/memory/Memory/mallocwrap.cpp:12

基本大纲是我的代码(在 C++ 中,低级部分除外,对于对 C 纯粹主义者造成的任何冒犯,我们深表歉意):

extern "C" void* malloc(const size_t size) __THROW
{
    return getMyAllocator().malloc(size);
}
// etc. for free() et al

// elsewhere
auto wrap(const char* sym)
{
    static void* libchandle = nullptr;
    auto f = dlsym(RTLD_NEXT,sym);
    if (f == nullptr)
   {
      std::fprintf(stderr, "error: unable to find symbol via dlsym(RTLD_NEXT,%s):\n",sym);
      std::fprintf(stderr, "%s\n",dlerror());
      f = dlsym(RTLD_DEFAULT, sym);
   }
   if (f == nullptr)
   {
      std::fprintf(stderr, "error: unable to find symbol via dlsym(RTLD_DEFAULT,%s):\n",sym);
      std::fprintf(stderr, "%s\n",dlerror());
      if (libchandle == nullptr)
      {
         libchandle = dlopen("libc.so", RTLD_LAZY);
         if (libchandle == nullptr)
         {                                                              \
            std::fprintf(stderr, "unable to open libc.so:\n");
            std::fprintf(stderr, "%s\n",dlerror());   
         }
         if (libchandle != nullptr)
         { 
            f = dlsym(libchandle, sym);
         } 
      }
      if (f == nullptr)
      {
         std::fprintf(stderr, "error: unable to find symbol via dlsym(\"libc\",%s):\n",sym); 
         std::fprintf(stderr, "%s\n",dlerror());
         std::exit(1);
      }
   }
   return f;
}

#define WRAP(X)                                 \
   { \
      static constexpr const char* const symName = #X;                 \
      auto f = reinterpret_cast<decltype(&::X)>(wrap(#X));             \
      this->X##Func = f; \
   } 

// Note: until ForwardingAllocator is setup
// malloc() etc are forwarded to __libc_malloc() etc
ForwardingAllocator::ForwardingAllocator()
{
   WRAP(malloc)
   WRAP(free)
   WRAP(calloc)
   WRAP(realloc)
   WRAP(malloc_usable_size)
}

为简洁起见省略了很多内容。

关于我可能做错了什么或如何更好地诊断问题有什么建议吗?

好像是jemalloc自己定义的__libc_malloc

>nm /usr/lib/debug/usr/lib64/libjemalloc.so.2-5.2.1-2.el8.x86_64.debug  | grep __libc_malloc
000000000000d4f0 t __libc_malloc

一些进一步的信息。

我成功处理过的并发症:

这是一个有用的答案 -

虽然 jemalloc 提供 __libc_malloc 作为符号,但它仅用于与 glibc 的静态链接。

当您转发到共享库中的 __libc_malloc 时,您仍在转发到 libc 实现。 但是,似乎在启动期间 jemalloc 将 malloc 挂钩设置为指向 malloc() 的先前地址。在这种情况下,第一个库(即你的)中的 malloc 包装器。 在内部设置了一些目前需要 3 次调用 malloc() 的东西之后,jemalloc 通过 libc malloc 挂钩将自己安装为新的 malloc

不幸的是,glibc 没有其他符号可以用来绕过 malloc 挂钩并直接使用 malloc。至少在我使用的版本上。

如果您有另一个 malloc 替代品要使用,您可以通过自己设置 malloc 挂钩来处理这个问题。但是,您已经表达了“做正确的事”而不是使用 malloc 挂钩的愿望,因为它们 deprecated

你可以在不使用 malloc hooks 的情况下处理这个问题 检测递归调用并提供到其他 malloc 的路径 例如:

   unsigned int malloc = 0;
   void* malloc(const size_t size)
   {
      if (inMalloc != 0) 
      {
         return handleRecursiveMalloc(size);
      }
      ++inMalloc;
      auto res = this->mainAllocator->malloc(size);
      --inMalloc;
      return res;
   }

   void* handleRecursiveMalloc(size_t size)
   {
      void* currentBreak = sbrk(0);
      if (currentBreak == nullptr)
      {
         return nullptr; // recursion detected and we could not handle it.
      }
      void* newBreak = sbrk(size);
      if (newBreak == nullptr)
      {
         return nullptr; // recursion detected and we could not handle it.
      }
      // we now have a block of memory between currentBreak & newBreak
      // book-keeping here if required
      //  emergencyAllocSize += size;
      //  numEmergencyAllocations++
      return currentBreak;
   }

这很丑陋,但很管用。你的 malloc 包装器在一次递增、一次递减和一次条件分支方面效率较低。 它可能没有任何区别,但您可以使用 C++ 属性 [[unlikely]] or gcc's __builtin_expect 表示不太可能采用递归分支。


还有一个陷阱需要注意。如果您转发多个符号,您应该检查它们是否全部安全转发(通常这意味着转发到同一个库)。 例如:

void* f1 = dlsym(RTLD_NEXT,"malloc");
void* f2 = dlsym(RTLD_NEXT,"malloc_usable_size");
// handle failures...
Dl_info info1;
dladdr(f1,&info1);
Dl_info info2;
dladdr(f2,&info2);
// handle failures...
if (info1.dli_fbase != info2.dli_fbase)
{
    // malloc_usable_size() is provided by a different library than malloc()
    // so we probably shouldn't use it
    f2 = nullptr; 
    // set flags accordingly
}

实践中的一个例子是 electric-fence。 如果我链:

LD_PRELOAD="mymalloc.so electric-fence.so"

你发现 malloc_usable_size() 来自 libcmalloc 来自 electric-fence。 当然 electric-fence 不再那么常见了。

在这种情况下,将 malloc_usable_size() 替换为始终 returns 0 的虚拟函数会更安全。 例如 malloc_usable_size(ptr) 的普通 libc 版本 -(参见 https://code.woboq.org/userspace/glibc/malloc/malloc.c.html)查看位于分配块之前的指针(即 ptr-2*sizeof(size_t) )。如果你给它一个不符合这个模式的指针,它可能会发生段错误。

参见示例Is it possible to define a symbol dynamically such that it will be found by dlsym?