diff --git a/CMakeLists.txt b/CMakeLists.txt index 71bd6d73..af60b732 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -389,8 +389,9 @@ if(USE_LOADER) target_link_libraries(dd_profiling-shared PRIVATE libddprofiling_embedded_object ddprof_exe_object) target_compile_definitions( - dd_profiling-shared PRIVATE DDPROF_EMBEDDED_LIB_DATA DDPROF_EMBEDDED_EXE_DATA - DDPROF_PROFILING_LIBRARY) + dd_profiling-shared + PRIVATE DDPROF_EMBEDDED_LIB_DATA DDPROF_EMBEDDED_EXE_DATA DDPROF_PROFILING_LIBRARY + DDPROF_LOADER_SONAME="lib$.so") else() # Without loader, libdd_profiling.so is basically the same as libdd_profiling-embedded.so plus an # embedded ddprof executable. diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md index 15eeac7d..d3e56acc 100644 --- a/docs/Troubleshooting.md +++ b/docs/Troubleshooting.md @@ -83,6 +83,24 @@ example: MALLOC_CONF=stats_print:true ./ddprof -S test-service service_cmd ``` +### Loading libdd_profiling.so via dlopen + +The recommended way to use `libdd_profiling.so` is to link against it directly +or use `LD_PRELOAD`. If `dlopen` is required (e.g. conditional profiling based +on a config flag): + +```c +dlopen("libdd_profiling.so", RTLD_NOW); +``` + +The loader internally promotes its own symbols to global scope so that the +embedded library it loads can resolve them. + +**musl limitation:** loading `libdd_profiling.so` with `dlopen` is not supported +on musl-based systems (e.g. Alpine Linux). musl rejects initial-exec TLS +cross-library relocations for `dlopen`'d libraries, which causes the embedded +library to fail to load. Use `LD_PRELOAD` instead on musl systems. + ### Library issues (LD_PRELOAD or allocation profiling) You will want to instrument the loader function (loader.c) to figure out what is going on. diff --git a/src/lib/loader.c b/src/lib/loader.c index ee47b197..3368b0aa 100644 --- a/src/lib/loader.c +++ b/src/lib/loader.c @@ -145,6 +145,35 @@ static void ensure_librt_is_loaded() { } } +// When the loader is dlopen'd with RTLD_GLOBAL, glibc does not promote its +// symbols to global scope until dlopen returns. The embedded .so references +// ddprof_lib_state (defined here in the loader) as an undefined symbol, so +// loading it with RTLD_NOW during our constructor fails with +// "undefined symbol: ddprof_lib_state". +// +// Fix: re-open ourselves with RTLD_NOLOAD | RTLD_GLOBAL to promote our +// symbols before loading the embedded .so. When loaded via LD_PRELOAD, +// symbols are already in global scope so this is a harmless no-op. +// +// Note: on musl, dlopen with RTLD_GLOBAL is not supported for this library +// because musl rejects initial-exec TLS cross-library relocations for +// dlopen'd libraries entirely. +static void ensure_loader_symbols_promoted() { +#ifdef DDPROF_LOADER_SONAME + void *self = + my_dlopen(DDPROF_LOADER_SONAME, RTLD_GLOBAL | RTLD_NOLOAD | RTLD_NOW); + if (!self) { + // RTLD_NOLOAD should always find the loader (we are running inside it). + // NULL means the soname has changed or something is seriously wrong; + // the embedded .so will likely fail to load next. + fprintf(stderr, + "ddprof loader: failed to promote symbols to global scope " + "(RTLD_NOLOAD on " DDPROF_LOADER_SONAME + " returned NULL) -- embedded library may fail to load\n"); + } +#endif +} + static const char *temp_directory_path() { const char *tmpdir = NULL; const char *env[] = {"TMPDIR", "TMP", "TEMP", "TEMPDIR", NULL}; @@ -279,6 +308,7 @@ static void __attribute__((constructor)) loader() { ensure_libm_is_loaded(); ensure_libpthread_is_loaded(); ensure_librt_is_loaded(); + ensure_loader_symbols_promoted(); s_profiling_lib_handle = my_dlopen(lib_profiling_path, RTLD_LOCAL | RTLD_NOW); free(lib_profiling_path); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index b5c02dee..eca30a43 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -534,6 +534,7 @@ if(NOT CMAKE_BUILD_TYPE STREQUAL "SanitizedDebug") COMMAND ${CMAKE_SOURCE_DIR}/tools/check_for_unsafe_libc_functions.py $) endif() + endif() if(NOT CMAKE_BUILD_TYPE STREQUAL "SanitizedDebug")