某些特殊场景下,我们可能会需要在动态库中使用与主程序不同的内存分配器,如主程序使用 glibc 默认的分配器,动态库使用 Jemalloc,显然使用 LD_PRELOAD
覆盖同名全局符号的方式不能满足这一需求,我们需要在动态库中静态链接 Jemalloc。但是,在动态库中静态链接使用 Jemalloc 需要特别注意 malloc/free, operator new/delete 等符号解析优先级的问题(Jemalloc 在 5.x 版本中引入了 C++ 的 operator new/delete 操作符重载,仅支持 C++14)。
不管是之前由动态链接器装入的,还是之后由 dlopen 装入的共享对象,动态链接器在进行符号解析以及重定位时,都是采用多个同名符号冲突时,先装入的符号优先(Load Ordering)的策略,因此在动态库中使用 Jemalloc 时为了避免符号污染(避免动态库引用主程序或其他动态库中的 malloc/free 或 operator new/delete,或将动态库中的 malloc/free 或 operator new/delete 暴露给外部),相关的选项包括:
- 为 malloc/free 等 C 内存管理接口增加前缀(
--with-jemalloc-prefix
); - 不导出 malloc/free 等 C 内存管理接口(
--without-export
); - 禁用 C++ operator new/delete 重载(
--disable-cxx
); - 通过内联汇编的方式隐藏重载的 C++ operator new/delete 符号(即不对动态库之外暴露这些符号);
- 动态库链接时加上
-Bsymbolic
或-Bsymbolic-functions
选项等。
考虑到对 C/C++ 程序内存管理方式的兼容,简单合理方式如下:对于 C 程序,通过同时使能 --without-export
和 --disable-cxx
实现,而对于 C++ 程序,则通过使能 --without-export
选项同时使用内联汇编隐藏重载的 C++ operator new/delete 符号来实现。
如果动态库需要与其它动态库进行交互,比如 IO 数据流交互,除非在接口处对数据全部进行一次内存拷贝,否则比如会出现内存由 glibc 分配然后由 Jemalloc 释放的情况,反之亦然,因此在动态库中使用 Jemalloc 的可行性其实非常非常小。
cmake/modules/BuildJemalloc.cmake
#
# CMake file for jemalloc
#
function(do_build_jemalloc)
set(jemalloc_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/jemalloc)
set(jemalloc_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/jemalloc)
set(jemalloc_INSTALL_DIR ${CMAKE_CURRENT_BINARY_DIR}/jemalloc/artifacts)
list(APPEND jemalloc_CONFIG_ARGS --prefix=${jemalloc_INSTALL_DIR})
list(APPEND jemalloc_CONFIG_ARGS --disable-initial-exec-tls)
list(APPEND jemalloc_CONFIG_ARGS --without-export)
#list(APPEND jemalloc_CONFIG_ARGS --disable-cxx)
#list(APPEND jemalloc_CONFIG_ARGS --with-jemalloc-prefix=je_)
#list(APPEND jemalloc_CONFIG_ARGS --enable-debug)
if(NOT EXISTS ${jemalloc_SOURCE_DIR}/configure)
execute_process(
COMMAND ./autogen.sh
WORKING_DIRECTORY ${jemalloc_SOURCE_DIR}
)
endif()
include(ExternalProject)
ExternalProject_Add(jemalloc-ext
PREFIX ${jemalloc_BINARY_DIR}
SOURCE_DIR ${jemalloc_SOURCE_DIR}
CONFIGURE_COMMAND CC=${CMAKE_C_COMPILER} CXX=${CMAKE_CXX_COMPILER}
<SOURCE_DIR>/configure ${jemalloc_CONFIG_ARGS}
BINARY_DIR ${jemalloc_BINARY_DIR}
BUILD_COMMAND $(MAKE)
BUILD_ALWAYS TRUE
BUILD_BYPRODUCTS ${jemalloc_LIBRARY}
INSTALL_DIR ${jemalloc_INSTALL_DIR}
INSTALL_COMMAND env -i $(MAKE) install
TEST_COMMAND true
)
endfunction()
macro(build_jemalloc)
do_build_jemalloc()
ExternalProject_Get_Property(jemalloc-ext INSTALL_DIR)
set(JEMALLOC_INCLUDE_DIR ${INSTALL_DIR}/include)
file(MAKE_DIRECTORY ${JEMALLOC_INCLUDE_DIR})
set(JEMALLOC_STATIC_LIBRARY jemalloc-static)
set(JEMALLOC_SHARED_LIBRARY jemalloc-shared)
add_library(jemalloc-static STATIC IMPORTED)
add_dependencies(jemalloc-static jemalloc-ext)
set_target_properties(jemalloc-static PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES ${JEMALLOC_INCLUDE_DIR}
IMPORTED_LINK_INTERFACE_LANGUAGES "C;CXX"
IMPORTED_LOCATION ${INSTALL_DIR}/lib/libjemalloc_pic.a
)
add_library(jemalloc-shared SHARED IMPORTED)
add_dependencies(jemalloc-shared jemalloc-ext)
set_target_properties(jemalloc-shared PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES ${JEMALLOC_INCLUDE_DIR}
IMPORTED_LOCATION ${INSTALL_DIR}/lib/libjemalloc.so
)
endmacro()
CMakeLists.txt
cmake_minimum_required(VERSION 2.8.12)
project(test_app)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules")
# build jemalloc
include(BuildJemalloc)
build_jemalloc()
# libabc
add_library(abc SHARED
src/libabc.cc
)
target_compile_options(abc PRIVATE
-std=c++14
-fno-builtin-malloc -fno-builtin-calloc -fno-builtin-realloc -fno-builtin-free
)
target_link_libraries(abc PRIVATE
${JEMALLOC_STATIC_LIBRARY}
pthread
#PRIVATE -Wl,-Bsymbolic
)
# test_app
add_executable(test_app src/test_app.cc)
target_compile_options(test_app PRIVATE
-std=c++14
)
target_link_libraries(test_app PRIVATE
dl
)
add_dependencies(test_app abc)
动态库对外隐藏 operator new/delete 符号
gcc libstdc++ 定义的 operator new/delete visibility 为 default(参见头文件 new),为了避免污染主程序和后续装入的动态库,因此我们要将 Jemalloc 中定义的 operator new/delete 的 visibility 定义为 hidden(malloc/free 等其他符号已通过 --without-export
选项隐藏)。
首先为 operator new/delete 生成 mangled 名字:
$ printf "#include <new>\nvoid *operator new(std::size_t size) {}" | g++ -x c++ -S - -o- | grep "^_.*:$" | sed -e 's/:$//'
_Znwm
$ printf "#include <new>\nvoid *operator new[](std::size_t size) {}" | g++ -x c++ -S - -o- | grep "^_.*:$" | sed -e 's/:$//'
_Znam
$ printf "#include <new>\nvoid *operator new(std::size_t size, const std::nothrow_t &) noexcept {}" | g++ -x c++ -S - -o- | grep "^_.*:$" | sed -e 's/:$//'
_ZnwmRKSt9nothrow_t
$ printf "#include <new>\nvoid *operator new[](std::size_t size, const std::nothrow_t &) noexcept {}" | g++ -x c++ -S - -o- | grep "^_.*:$" | sed -e 's/:$//'
_ZnamRKSt9nothrow_t
$ printf "#include <new>\nvoid operator delete(void *ptr) noexcept {}" | g++ -x c++ -S - -o- | grep "^_.*:$" | sed -e 's/:$//'
_ZdlPv
$ printf "#include <new>\nvoid operator delete[](void *ptr) noexcept {}" | g++ -x c++ -S - -o- | grep "^_.*:$" | sed -e 's/:$//'
_ZdaPv
$ printf "#include <new>\nvoid operator delete(void *ptr, const std::nothrow_t &) noexcept {}" | g++ -x c++ -S - -o- | grep "^_.*:$" | sed -e 's/:$//'
_ZdlPvRKSt9nothrow_t
$ printf "#include <new>\nvoid operator delete[](void *ptr, const std::nothrow_t &) noexcept {}" | g++ -x c++ -S - -o- | grep "^_.*:$" | sed -e 's/:$//'
_ZdaPvRKSt9nothrow_t
$ printf "#include <new>\nvoid operator delete(void *ptr, std::size_t size) noexcept {}" | g++ -x c++ -S - -o- | grep "^_.*:$" | sed -e 's/:$//'
_ZdlPvm
$ printf "#include <new>\nvoid operator delete[](void *ptr, std::size_t size) noexcept {}" | g++ -x c++ -S - -o- | grep "^_.*:$" | sed -e 's/:$//'
_ZdaPvm
然后在动态库的 cpp 文件中通过内联汇编的形式将这些 C++ operator new/delete 符号的 visibility 定义为 hidden:
// void *operator new(std::size_t size)
asm(".hidden _Znwm");
// void *operator new[](std::size_t size)
asm(".hidden _Znam");
// void *operator new(std::size_t size, const std::nothrow_t &) noexcept
asm(".hidden _ZnwmRKSt9nothrow_t");
// void *operator new[](std::size_t size, const std::nothrow_t &) noexcept
asm(".hidden _ZnamRKSt9nothrow_t");
// void operator delete(void *ptr) noexcept
asm(".hidden _ZdlPv");
// void operator delete[](void *ptr) noexcept
asm(".hidden _ZdaPv");
// void operator delete(void *ptr, const std::nothrow_t &) noexcept
asm(".hidden _ZdlPvRKSt9nothrow_t");
// void operator delete[](void *ptr, const std::nothrow_t &) noexcept
asm(".hidden _ZdaPvRKSt9nothrow_t");
// void operator delete(void *ptr, std::size_t size) noexcept
asm(".hidden _ZdlPvm");
// void operator delete[](void *ptr, std::size_t size) noexcept
asm(".hidden _ZdaPvm");
strdup/strndup
如果在动态库的代码里使用了 strdup/strndup
之类的接口的话,需要特别注意 Jemalloc 并没有提供它们的实现:
$ readelf --dyn-syms libabc.so | grep strdup
54: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strdup@GLIBC_2.2.5 (2)
strdup/strndup
在内部进行内存分配时使用 glibc 的 malloc 接口(先装入符号优先),而内存释放是在动态库代码中使用 free 接口释放,因此,会出现 glibc 分配的内存由 Jemalloc 释放的情况。
要解决这个问题,比较简单的方法就是和上面 operator new/delete 一样,在动态库代码中实现 strdup/strndup
接口,并通过内联汇编将其 visibility 设置成 hidden,还一种方法就是通过 --with-jemalloc-prefix
选项给 Jemalloc 的内存管理接口加上前缀,这样就不会隐式的覆盖动态库中使用的 glibc 内存管理接口,当然如果这些接口仍然要使用 Jemalloc 的实现的话,就需要显式的调用加上了前缀的 Jemalloc 接口。
需要注意的是,如果在动态库中引用了第三方库,分析这些内存的分配和释放的接口是否匹配会变得非常复杂,如果实在难以处理,显式调用 Jemalloc 的接口进行内存管理会是更靠谱的办法。
参考资料
static TLS errors from jemalloc 5.0.0 built on CentOS 6
https://github.com/jemalloc/jemalloc/issues/937
initial-exec TLS model breaks dlopen’ed libGL
https://bugs.freedesktop.org/show_bug.cgi?id=35268
Thread-Local Storage Descriptors for IA32 and AMD64/EM64T
http://www.fsfla.org/~lxoliva/writeups/TLS/RFC-TLSDESC-x86.txt
Using the GNU Compiler Collection (GCC) - 6.34.1 Common Variable Attributes
https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html#Common-Variable-Attributes
Provide -fvisibility-global-new-delete-hidden option
https://reviews.llvm.org/D53787
using as - the gnu assembler - 7.41 .hidden names
https://sourceware.org/binutils/docs-2.33.1/as/Hidden.html#Hidden
Getting mangled name from demangled name
https://stackoverflow.com/questions/12400105/getting-mangled-name-from-demangled-name
Demangling C++ Symbols
https://fitzgeraldnick.com/2017/02/22/cpp-demangle.html
TCMalloc readme
https://github.com/gperftools/gperftools/blob/master/README
最后修改于 2019-10-18