xDL resolves symbols Android's linker namespace deliberately hides
:fire: xDL is an enhanced implementation of the Android DL series functions.
At a glance
- What is it?
- xDL is a small C library that reimplements dlopen, dlsym, dladdr and dl_iterate_phdr so that native code can look up symbols the Android 7.0 linker namespace would otherwise keep out of reach, including symbols in .symtab and in .gnu_debugdata. It ships as a Prefab package on Maven Central, supports API 16 to 37 and four ABIs, and is MIT licensed.
- Who is it for?
- Adopt xDL if you are writing native Android code that must resolve symbols at runtime outside the reach of the platform linker, or if you need to read debug symbols from .symtab and .gnu_debugdata rather than only the dynamic symbol table, since that is what the library adds over the standard calls.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 155 days ago.
- What is it written in?
- Mainly C, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Why the stock linker functions are not enough on Android
xDL is described as an enhanced implementation of the Android DL series functions, and the enhancement is aimed at one specific platform behaviour: the linker namespace introduced in Android 7.0.
The namespace mechanism restricts which shared libraries a given process can load. A library in one namespace is invisible to code in another, and on Android that means a dlopen() from your own code may fail for a library that plainly exists on the device. The first feature entry says the enhanced dlopen, dlsym and dladdr can bypass the restrictions of the Android 7.0+ linker namespace, which is the reason the project exists at all.
The second part of the feature is symbol lookup. The stock dlsym only sees the dynamic symbol table, .dynsym, because that is what a dynamic linker needs to resolve at load time. xDL can also look up debugging symbols in .symtab and in the .symtab inside .gnu_debugdata, which is the compressed debug section GNU toolchains produce to keep a separate debug file small. A function compiled and stripped of its dynamic export can still be found this way.
The third enhancement is dl_iterate_phdr, and here the fixes are about older devices and about what the function returns. The README lists compatibility with Android 4.x on ARM32, inclusion of linker and linker64 for Android 8.x and earlier, returning the full pathname instead of the basename for Android 5.x, and returning app_process32 or app_process64 instead of the package name.
The supported range is Android 4.1 to 17, API level 16 to 37, across armeabi-v7a, arm64-v8a, x86 and x86_64, and the licence is MIT.
The three load flags decide whether a library is actually loaded
The API section is where the behaviour of the library becomes concrete, and the flag semantics are the part to read carefully.
The header is included with:
#include "xdl.h"And the open and close functions are:
#define XDL_DEFAULT 0x00
#define XDL_TRY_FORCE_LOAD 0x01
#define XDL_ALWAYS_FORCE_LOAD 0x02
void *xdl_open(const char *filename, int flags);
void *xdl_open2(struct dl_phdr_info *info);
void *xdl_close(void *handle);The three flags give three different answers to the question of whether to call dlopen. With XDL_DEFAULT, if the library has already been loaded into memory, xdl_open() will not dlopen() it again, though it will still return a valid handle. With XDL_TRY_FORCE_LOAD, if the library has not been loaded into memory, xdl_open() will try to dlopen() it. With XDL_ALWAYS_FORCE_LOAD, xdl_open() will always dlopen() the library.
The consequence is stated precisely: if xdl_open() really used dlopen() to load the library, xdl_close() will return the handle from the linker, and you can then decide whether and when to close it with a standard dlclose(). Otherwise, NULL is returned. So the close function is not a drop-in for dlclose, and the return value tells you which case you are in.
The filename argument can be a basename or a full pathname, and the README explains the constraint that follows from the namespace mechanism. If you pass a basename you must make sure no duplicate ELF is loaded into the current process, because xdl_open() will only return the first matching ELF. The README illustrates this with a fragment of /proc/self/maps on Android 10, where the same libc++.so appears three times from /system/lib64/vndk-sp-29, from the conscrypt APEX and from /system/lib64, and a basename search would return whichever came first in the mapping rather than the one you meant.
Consuming it as a Prefab package from Maven Central
xDL is published on Maven Central and uses the Prefab package format for native dependencies, which the README says is supported by Android Gradle Plugin 4.0 and later. The Gradle side is short:
android {
buildFeatures {
prefab true
}
}
dependencies {
implementation 'io.github.hexhacking:xdl:2.4.0'
}The native side depends on whether the project uses CMake or the older ndk-build. With CMake:
find_package(xdl REQUIRED CONFIG)
add_library(mylib SHARED mylib.c)
target_link_libraries(mylib xdl::xdl)With Android.mk the module is declared and the prefab module imported:
include $(CLEAR_VARS)
LOCAL_MODULE := mylib
LOCAL_SRC_FILES := mylib.c
LOCAL_SHARED_LIBRARIES += xdl
include $(BUILD_SHARED_LIBRARY)
$(call import-module,prefab/xdl)Two version details are documented as notes and both will bite someone. First, the group ID changed from io.hexhacking to io.github.hexhacking starting from version 2.0.0, and the README gives the coordinate table: versions 1.0.3 to 1.2.1 use io.hexhacking with the xdl artifact, and 2.0.0 onwards use io.github.hexhacking with the same artifact ID, each pointing at its own repository path.
Second, xDL uses the prefab package schema v2, which is configured by default since Android Gradle Plugin 7.1.0. If your plugin is earlier than that, the README says to add android.prefabVersion=2.0.0 to gradle.properties.
ABI filters and the duplicate libxdl.so problem
Step three of the usage guide is selecting the ABIs you ship, and the answer is a list of the four the project supports:
android {
defaultConfig {
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
}
}
}Step four is the one that causes real build friction, and the reason is the same in both directions: xDL is compiled as an independent dynamic library, so libxdl.so ends up in your output, and if another dependency also ships one you get a duplicate at packaging time. The fix differs by project type.
In an SDK project, you may need to avoid packaging libxdl.so into your AAR, so the README suggests excluding it in the packagingOptions block with an exclude rule for the libxdl.so path. In an app project, the conflict is the other way round, since you are consuming an AAR that already contains one, so the README suggests a pickFirst rule for the same path instead.
There is a sample app in the xdl_sample folder in the repository, which is the fastest way to see a working configuration of all four steps together.
The size table is worth a glance too, because the numbers are small. Compiled into an independent dynamic library, the compressed sizes are 8.3 KB for armeabi-v7a, 9.1 KB for arm64-v8a, 9.4 KB for x86 and 9.3 KB for x86_64, with uncompressed sizes between 15 and 20 KB. For a library whose whole purpose is to see things the linker hides, that footprint is modest.
Where xDL is the wrong tool
There are three honest limits, and the first is the one that decides most cases.
The first is that the namespace bypass is only worth having if you actually need it. If your code is doing ordinary dynamic loading inside its own application namespace, the stock dlopen and dlsym work, they are well documented, and swapping them for xDL adds a dependency, an ABI matrix, a Maven coordinate and a duplicate-library packaging problem in exchange for nothing. The cost side of that trade is not trivial in a build system, because the prefab wiring and the packaging options touch every module that links the library.
The second is that symbol lookup by pattern is inherently approximate. The library can look up debugging symbols in .symtab and in .gnu_debugdata, which is what makes it able to find functions that were never exported dynamically. But the reason the linker does not export them is usually that the platform does not want them reachable, and code that depends on a symbol found this way is depending on an implementation detail of how a particular library was built and stripped. If the vendor changes their build flags, your lookup stops resolving, and the failure appears as a null pointer rather than as an error at link time.
The third is platform coverage. The README states support for Android 4.1 to 17 and the four ABIs, and the badges in the header say the same. There is no mention of iOS, Linux or Windows, and the implementation depends on Android linker behaviour, including the /proc/self/maps parsing and the phdr iteration that the dl_iterate_phdr enhancements are built on. This is an Android library, and using it elsewhere is not a supported path.
The alternative approach is to use the standard calls plus a different strategy. If the goal is instrumentation or hooking rather than symbol resolution, there is a whole category of tools built on other mechanisms, such as dynamic binary instrumentation or the platform's own debugging interfaces, and those are documented and supported rather than reverse engineered. The difference in approach is that xDL works inside the target process and reaches past the linker's restrictions, while the alternatives work from outside or through a sanctioned interface, and the choice usually comes down to whether you need to be inside the namespace or not.
Editorial conclusion
Adopt xDL if you are writing native Android code that must resolve symbols at runtime outside the reach of the platform linker, or if you need to read debug symbols from .symtab and .gnu_debugdata rather than only the dynamic symbol table, since that is what the library adds over the standard calls. Do not adopt it if your code only needs ordinary dlopen and dlsym within the app's own namespace, and do not adopt it without reading the symbol-lookup implications on a device you do not control. Verify first that your build uses Android Gradle Plugin 4.0 or later with the prefab package schema v2 configured, that the ABI filters you set match the devices you ship to, and that the group ID you use is io.github.hexhacking, since the coordinates changed at version 2.0.0 and an old coordinate will silently resolve to a different artefact.
Frequently asked questions
What does xDL add to the standard Android dlopen and dlsym?
The enhanced dlopen, dlsym and dladdr can bypass the restrictions of the Android 7.0+ linker namespace, the symbol lookup can find dynamic link symbols in .dynsym and debugging symbols in .symtab and in the .symtab inside .gnu_debugdata, and the enhanced dl_iterate_phdr is compatible with Android 4.x on ARM32 and returns fuller path information.
How do I add xDL to an Android project?
xDL is published on Maven Central and uses the Prefab package format. Enable prefab in the buildFeatures block, add the dependency io.github.hexhacking:xdl:2.4.0, then link it from CMake with find_package(xdl REQUIRED CONFIG) and target_link_libraries(mylib xdl::xdl), or from Android.mk with the import-module call.
What is the difference between the XDL_DEFAULT, XDL_TRY_FORCE_LOAD and XDL_ALWAYS_FORCE_LOAD flags?
XDL_DEFAULT will not dlopen() a library that is already in memory, though it still returns a valid handle. XDL_TRY_FORCE_LOAD will try to dlopen() the library if it has not been loaded into memory. XDL_ALWAYS_FORCE_LOAD always dlopen()s the library, and in that case xdl_close() returns the linker handle so you can close it with a standard dlclose().
Why does the xDL group ID matter when upgrading?
Starting from version 2.0.0 the group ID changed from io.hexhacking to io.github.hexhacking, and the README gives the table: 1.0.3 to 1.2.1 use io.hexhacking, and 2.0.0 onwards use io.github.hexhacking, with the artifact ID xdl in both cases but different repository paths.
How do I avoid a duplicate libxdl.so at packaging time?
In an SDK project the README suggests excluding it with an exclude rule in the packagingOptions block so it is not packaged into your AAR. In an app project, where a dependency may already contain one, it suggests a pickFirst rule for the same path instead. A sample app covering all setup steps is in the xdl_sample folder.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/hexhacking-xdl)Was this useful?