runsisi's

technical notes

在动态库中使用 Jemalloc

2019-10-18 runsisi#cpp

某些特殊场景下,我们可能会需要在动态库中使用与主程序不同的内存分配器,如主程序使用 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

https://github.com/vsrinivas/fuchsia/blob/5f55d90d0a3a1fa7d42808aa08129bff97940215/zircon/system/ulib/zxcpp/new.cpp

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