-
Notifications
You must be signed in to change notification settings - Fork 294
Description
(Sorry if this isn't the right place to ask this question, and I'd appreciate a pointer to the right place if so.)
I've been trying to understand the bionic dynamic linker's symbol resolution order, as detailed in https://android.googlesource.com/platform/bionic/+/master/android-changes-for-ndk-developers.md. In particular, I'm interested in symbol resolution when multiple libraries define the same symbol. Here's my initial test case.
CMakeLists.txt:
cmake_minimum_required(VERSION 3.10.2)
project(dltest C)
add_library(a1 SHARED a1.c)
add_library(a2 SHARED a2.c)
add_library(b SHARED b.c)
target_link_libraries(b PRIVATE a2)
add_library(d SHARED d.c)
target_link_libraries(d PRIVATE a1 b)
add_executable(main main.c)
a1.c:
int a() { return 1; }
a2.c:
int a() { return 2; }
b.c:
int a(void);
int b() { return a(); }
d.c:
int b(void);
int d() { return b(); }
main.c:
#include <dlfcn.h>
#include <stdio.h>
typedef int (*d_type)(void);
int main() {
void *libd = dlopen("libd.so", RTLD_NOW | RTLD_LOCAL);
d_type d_ptr = (d_type)dlsym(libd, "d");
printf("%d\n", d_ptr());
return 0;
}
In other words, liba1.so and liba2.so both define a function a. libb.so has a function b which calls a, and it's linked against liba2.so. libd.so has a function d which calls b, and it's linked against both liba1.so and libb.so. (Its DT_NEEDED will be in that order as well, i.e. it'll have liba1.so and then libb.so.) My main executable dynamically loads libd.so and calls d. What I'm trying to determine here is whether symbol resolution for a library only cares about the NEEDED libraries of the library itself, or also takes the NEEDED libraries of the library which loaded that library into account.
I'm building this using the NDK r21-provided CMake toolchain. When I run the program on an API 22 emulator, it prints 2. When I run it on an API 23 emulator, it prints 1. I'm assuming that the relevant linker change is the following:
Before API 23, the default search order was to try the main executable, LD_PRELOAD libraries, the library itself, and its DT_NEEDED libraries in that order. For API 23 and later, for any given library, the dynamic linker divides other libraries into the global group and the local group. The global group is shared by all libraries and contains the main executable, LD_PRELOAD libraries, and any library with the DF_1_GLOBAL flag set (by passing “-z global” to ld(1)). The local group is the breadth-first transitive closure of the library and its DT_NEEDED libraries. The M dynamic linker searches the global group followed by the local group. This allows ASAN, for example, to ensure that it can intercept any symbol.
In other words, on API < 23, only the DT_NEEDED of the library itself (libb.so in my case) would be considered. For API >= 23, it's the breadth-first transitive closure of the library which is being loaded (libd.so in my case). Is this a correct interpretation of what's happening here?
I've also tested this with an actual app using System.loadLibrary with the same results, so it's not specific to just a native executable. A related question though; in the context of an app, what does the "main executable" refer to (e.g. when the documentation states "The global group is shared by all libraries and contains the main executable")?