如何正确插入允许 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
一些进一步的信息。
- malloc_hook已弃用,因此我不使用它们。
我成功处理过的并发症:
dlsym() 使用 malloc() - 我在启动期间使用一个简单的 bootstrap 分配器,然后切换到转发到 libc 的 malloc()
的主分配器
我最初使用一个简单的分配器作为 booststrap 分配器
我对 free() 的包装委托给适当的 free(),具体取决于使用的是哪个 malloc()
我现在已经开始使用 __libc_malloc 作为 bootstrap 分配器,但允许尽快通过 dlsym 替换它。
这是一个有用的答案 -
虽然 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()
来自 libc
而 malloc
来自 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?
我创建了一个共享库,其中插入了 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
一些进一步的信息。
- malloc_hook已弃用,因此我不使用它们。
我成功处理过的并发症:
dlsym() 使用 malloc() - 我在启动期间使用一个简单的 bootstrap 分配器,然后切换到转发到 libc 的 malloc()
的主分配器我最初使用一个简单的分配器作为 booststrap 分配器
我对 free() 的包装委托给适当的 free(),具体取决于使用的是哪个 malloc()
我现在已经开始使用 __libc_malloc 作为 bootstrap 分配器,但允许尽快通过 dlsym 替换它。
这是一个有用的答案 -
虽然 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()
来自 libc
而 malloc
来自 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?