Introduction to SO Files¶
Basic Introduction¶
- Why use Shared Objects (SO)?
- Development efficiency
- Quick porting
- SO versions
- Vary depending on the CPU platform
Loading Methods¶
- System.loadLibrary
- If the file name to load is xxx, it actually loads the libxxx.so file in the project's libs directory.
- System.load
- Corresponds to the absolute path of the lib.
The first method is mainly used; the second method is primarily used for loading SO files in plugins.
loadLibrary Loading Process¶
According to the official API description:
The call
System.loadLibrary(name)is effectively equivalent to the callRuntime.getRuntime().loadLibrary(name)
As we can see, this function actually calls the loadLibrary function in Runtime.java (libcore/luni/src/main/java/java/lang/Runtime.java), which then continues to call another overloaded loadLibrary function with two parameters:
- libName, the library name we pass in
- VMStack.getCallingClassLoader(), the ClassLoader, which facilitates finding the corresponding library.
/**
* Loads and links the library with the specified name. The mapping of the
* specified library name to the full path for loading the library is
* implementation-dependent.
*
* @param libName
* the name of the library to load.
* @throws UnsatisfiedLinkError
* if the library can not be loaded.
*/
public void loadLibrary(String libName) {
loadLibrary(libName, VMStack.getCallingClassLoader());
}
/*
* Searches for a library, then loads and links it without security checks.
*/
void loadLibrary(String libraryName, ClassLoader loader) {
if (loader != null) {
String filename = loader.findLibrary(libraryName);
if (filename == null) {
throw new UnsatisfiedLinkError("Couldn't load " + libraryName +
" from loader " + loader +
": findLibrary returned null");
}
String error = doLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
String filename = System.mapLibraryName(libraryName);
List<String> candidates = new ArrayList<String>();
String lastError = null;
for (String directory : mLibPaths) {
String candidate = directory + filename;
candidates.add(candidate);
if (IoUtils.canOpenReadOnly(candidate)) {
String error = doLoad(candidate, loader);
if (error == null) {
return; // We successfully loaded the library. Job done.
}
lastError = error;
}
}
if (lastError != null) {
throw new UnsatisfiedLinkError(lastError);
}
throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
}
As we can see, the main functionality of the program is exactly as the comment states:
Searches for a library, then loads and links it without security checks.
The loading function used here is the doLoad function. At this point, let's not continue the analysis; instead, let's take a look at how the load function works.
load Loading Process¶
According to the official API description:
The call System.load(name) is effectively equivalent to the call:
java Runtime.getRuntime().load(name)
It similarly calls a function in Runtime.java, as follows:
/**
* Loads and links the dynamic library that is identified through the
* specified path. This method is similar to {@link #loadLibrary(String)},
* but it accepts a full path specification whereas {@code loadLibrary} just
* accepts the name of the library to load.
*
* @param pathName
* the absolute (platform dependent) path to the library to load.
* @throws UnsatisfiedLinkError
* if the library can not be loaded.
*/
public void load(String pathName) {
load(pathName, VMStack.getCallingClassLoader());
}
/*
* Loads and links the given library without security checks.
*/
void load(String pathName, ClassLoader loader) {
if (pathName == null) {
throw new NullPointerException("pathName == null");
}
String error = doLoad(pathName, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
}
It similarly calls the two-parameter overloaded load function, which in turn calls the doLoad function.
Regardless of which loading method is used above, the doLoad function in Runtime.java is ultimately called.
Core Loading Process¶
doLoad¶
Now let's analyze the doLoad function:
private String doLoad(String name, ClassLoader loader) {
// Android apps are forked from the zygote, so they can't have a custom LD_LIBRARY_PATH,
// which means that by default an app's shared library directory isn't on LD_LIBRARY_PATH.
// The PathClassLoader set up by frameworks/base knows the appropriate path, so we can load
// libraries with no dependencies just fine, but an app that has multiple libraries that
// depend on each other needed to load them in most-dependent-first order.
// We added API to Android's dynamic linker so we can update the library path used for
// the currently-running process. We pull the desired path out of the ClassLoader here
// and pass it to nativeLoad so that it can call the private dynamic linker API.
// We didn't just change frameworks/base to update the LD_LIBRARY_PATH once at the
// beginning because multiple apks can run in the same process and third party code can
// use its own BaseDexClassLoader.
// We didn't just add a dlopen_with_custom_LD_LIBRARY_PATH call because we wanted any
// dlopen(3) calls made from a .so's JNI_OnLoad to work too.
// So, find out what the native library search path is for the ClassLoader in question...
String ldLibraryPath = null;
if (loader != null && loader instanceof BaseDexClassLoader) {
ldLibraryPath = ((BaseDexClassLoader) loader).getLdLibraryPath();
}
// nativeLoad should be synchronized so there's only one LD_LIBRARY_PATH in use regardless
// of how many ClassLoaders are in the system, but dalvik doesn't support synchronized
// internal natives.
synchronized (this) {
return nativeLoad(name, loader, ldLibraryPath);
}
}
Although the source code is quite long, much of it consists of comments that explain why such a function is needed. The main reasons are:
- Android Apps are all forked from zygote, so their LD_LIBRARY_PATH is the same as zygote's LD_LIBRARY_PATH, which means the SO files in an APK are not located in this path.
- SO files may have mutual dependencies, so we need to load them in reverse order of their dependency relationships.
The basic idea of the function is to find the library file's path, and then call the nativeLoad function in a synchronized manner.
nativeLoad¶
The nativeLoad function is actually a native layer function:
// TODO: should be synchronized, but dalvik doesn't support synchronized internal natives.
private static native String nativeLoad(String filename, ClassLoader loader,
String ldLibraryPath);
The corresponding file path is dalvik/vm/native/java_lang_Runtime.cpp. The specific nativeLoad function is as follows:
const DalvikNativeMethod dvm_java_lang_Runtime[] = {
{ "freeMemory", "()J",
Dalvik_java_lang_Runtime_freeMemory },
{ "gc", "()V",
Dalvik_java_lang_Runtime_gc },
{ "maxMemory", "()J",
Dalvik_java_lang_Runtime_maxMemory },
{ "nativeExit", "(I)V",
Dalvik_java_lang_Runtime_nativeExit },
{ "nativeLoad", "(Ljava/lang/String;Ljava/lang/ClassLoader;Ljava/lang/String;)Ljava/lang/String;",
Dalvik_java_lang_Runtime_nativeLoad },
{ "totalMemory", "()J",
Dalvik_java_lang_Runtime_totalMemory },
{ NULL, NULL, NULL },
};
As we can see, the corresponding function at the native layer is Dalvik_java_lang_Runtime_nativeLoad:
/*
* static String nativeLoad(String filename, ClassLoader loader, String ldLibraryPath)
*
* Load the specified full path as a dynamic library filled with
* JNI-compatible methods. Returns null on success, or a failure
* message on failure.
*/
static void Dalvik_java_lang_Runtime_nativeLoad(const u4* args,
JValue* pResult)
{
StringObject* fileNameObj = (StringObject*) args[0];
Object* classLoader = (Object*) args[1];
StringObject* ldLibraryPathObj = (StringObject*) args[2];
assert(fileNameObj != NULL);
char* fileName = dvmCreateCstrFromString(fileNameObj);
if (ldLibraryPathObj != NULL) {
char* ldLibraryPath = dvmCreateCstrFromString(ldLibraryPathObj);
void* sym = dlsym(RTLD_DEFAULT, "android_update_LD_LIBRARY_PATH");
if (sym != NULL) {
typedef void (*Fn)(const char*);
Fn android_update_LD_LIBRARY_PATH = reinterpret_cast<Fn>(sym);
(*android_update_LD_LIBRARY_PATH)(ldLibraryPath);
} else {
ALOGE("android_update_LD_LIBRARY_PATH not found; .so dependencies will not work!");
}
free(ldLibraryPath);
}
StringObject* result = NULL;
char* reason = NULL;
bool success = dvmLoadNativeCode(fileName, classLoader, &reason);
if (!success) {
const char* msg = (reason != NULL) ? reason : "unknown failure";
result = dvmCreateStringFromCstr(msg);
dvmReleaseTrackedAlloc((Object*) result, NULL);
}
free(reason);
free(fileName);
RETURN_PTR(result);
}
Based on the comments, we can determine that the key code is:
bool success = dvmLoadNativeCode(fileName, classLoader, &reason);
After this line executes, it tells us whether loading the corresponding SO was successful.
dvmLoadNativeCode¶
The basic code is as follows. We can use the comments to briefly assess the function's purpose:
- The program loads the corresponding native code based on the specified absolute path, but if the library has already been loaded, it will not be loaded again.
Furthermore, as stated in the JNI specification, we cannot load a library into multiple class loaders — that is, a library can only be associated with one class loader.
The basic execution flow of the function is as follows:
- Use findSharedLibEntry to check whether this library has already been loaded, and if so, whether the same class loader was used.
/*
* Load native code from the specified absolute pathname. Per the spec,
* if we've already loaded a library with the specified pathname, we
* return without doing anything.
*
* TODO? for better results we should absolutify the pathname. For fully
* correct results we should stat to get the inode and compare that. The
* existing implementation is fine so long as everybody is using
* System.loadLibrary.
*
* The library will be associated with the specified class loader. The JNI
* spec says we can't load the same library into more than one class loader.
*
* Returns "true" on success. On failure, sets *detail to a
* human-readable description of the error or NULL if no detail is
* available; ownership of the string is transferred to the caller.
*/
bool dvmLoadNativeCode(const char* pathName, Object* classLoader,
char** detail)
{
SharedLib* pEntry;
void* handle;
bool verbose;
/* reduce noise by not chattering about system libraries */
verbose = !!strncmp(pathName, "/system", sizeof("/system")-1);
verbose = verbose && !!strncmp(pathName, "/vendor", sizeof("/vendor")-1);
if (verbose)
ALOGD("Trying to load lib %s %p", pathName, classLoader);
*detail = NULL;
/*
* See if we've already loaded it. If we have, and the class loader
* matches, return successfully without doing anything.
*/
pEntry = findSharedLibEntry(pathName);
if (pEntry != NULL) {
if (pEntry->classLoader != classLoader) {
ALOGW("Shared lib '%s' already opened by CL %p; can't open in %p",
pathName, pEntry->classLoader, classLoader);
return false;
}
if (verbose) {
ALOGD("Shared lib '%s' already loaded in same CL %p",
pathName, classLoader);
}
if (!checkOnLoadResult(pEntry))
return false;
return true;
}
- If it has not been loaded, dlopen is used to open the shared library.
/*
* Open the shared library. Because we're using a full path, the system
* doesn't have to search through LD_LIBRARY_PATH. (It may do so to
* resolve this library's dependencies though.)
*
* Failures here are expected when java.library.path has several entries
* and we have to hunt for the lib.
*
* The current version of the dynamic linker prints detailed information
* about dlopen() failures. Some things to check if the message is
* cryptic:
* - make sure the library exists on the device
* - verify that the right path is being opened (the debug log message
* above can help with that)
* - check to see if the library is valid (e.g. not zero bytes long)
* - check config/prelink-linux-arm.map to ensure that the library
* is listed and is not being overrun by the previous entry (if
* loading suddenly stops working on a prelinked library, this is
* a good one to check)
* - write a trivial app that calls sleep() then dlopen(), attach
* to it with "strace -p <pid>" while it sleeps, and watch for
* attempts to open nonexistent dependent shared libs
*
* This can execute slowly for a large library on a busy system, so we
* want to switch from RUNNING to VMWAIT while it executes. This allows
* the GC to ignore us.
*/
Thread* self = dvmThreadSelf();
ThreadStatus oldStatus = dvmChangeStatus(self, THREAD_VMWAIT);
handle = dlopen(pathName, RTLD_LAZY);
dvmChangeStatus(self, oldStatus);
if (handle == NULL) {
*detail = strdup(dlerror());
ALOGE("dlopen(\"%s\") failed: %s", pathName, *detail);
return false;
}
The dlopen function (bionic/linker/dlfcn.cpp) is as follows:
void* dlopen(const char* filename, int flags) {
ScopedPthreadMutexLocker locker(&gDlMutex);
soinfo* result = do_dlopen(filename, flags);
if (result == NULL) {
__bionic_format_dlerror("dlopen failed", linker_get_error_buffer());
return NULL;
}
return result;
}
It calls the do_dlopen function (bionic/linker/linker.cpp):
soinfo* do_dlopen(const char* name, int flags) {
if ((flags & ~(RTLD_NOW|RTLD_LAZY|RTLD_LOCAL|RTLD_GLOBAL)) != 0) {
DL_ERR("invalid flags to dlopen: %x", flags);
return NULL;
}
set_soinfo_pool_protection(PROT_READ | PROT_WRITE);
soinfo* si = find_library(name); // Check if the library exists; if so, complete initialization
if (si != NULL) {
si->CallConstructors();
}
set_soinfo_pool_protection(PROT_READ);
return si;
}
After finding the corresponding library, si->CallConstructors(); is used to construct the relevant information:
void soinfo::CallConstructors() {
if (constructors_called) {
return;
}
// We set constructors_called before actually calling the constructors, otherwise it doesn't
// protect against recursive constructor calls. One simple example of constructor recursion
// is the libc debug malloc, which is implemented in libc_malloc_debug_leak.so:
// 1. The program depends on libc, so libc's constructor is called here.
// 2. The libc constructor calls dlopen() to load libc_malloc_debug_leak.so.
// 3. dlopen() calls the constructors on the newly created
// soinfo for libc_malloc_debug_leak.so.
// 4. The debug .so depends on libc, so CallConstructors is
// called again with the libc soinfo. If it doesn't trigger the early-
// out above, the libc constructor will be called again (recursively!).
constructors_called = true;
if ((flags & FLAG_EXE) == 0 && preinit_array != NULL) {
// The GNU dynamic linker silently ignores these, but we warn the developer.
PRINT("\"%s\": ignoring %d-entry DT_PREINIT_ARRAY in shared library!",
name, preinit_array_count);
}
if (dynamic != NULL) {
for (Elf32_Dyn* d = dynamic; d->d_tag != DT_NULL; ++d) {
if (d->d_tag == DT_NEEDED) {
const char* library_name = strtab + d->d_un.d_val;
TRACE("\"%s\": calling constructors in DT_NEEDED \"%s\"", name, library_name);
find_loaded_library(library_name)->CallConstructors(); // Check if the library is already loaded
}
}
}
TRACE("\"%s\": calling constructors", name);
// DT_INIT should be called before DT_INIT_ARRAY if both are present.
CallFunction("DT_INIT", init_func);
CallArray("DT_INIT_ARRAY", init_array, init_array_count, false);
}
As we can see, just as the comments describe, if the .init function and init_array exist, the program will sequentially call the .init function and the code at the corresponding positions in .init_array. The relevant definitions are as follows:
#define DT_INIT 12 /* Address of initialization function */
#define DT_INIT_ARRAY 25 /* Address of initialization function array */
- Create an entry for the opened shared library and attempt to add it to the corresponding list for management. If the addition fails, the entry is freed.
/* create a new entry */
SharedLib* pNewEntry;
pNewEntry = (SharedLib*) calloc(1, sizeof(SharedLib));
pNewEntry->pathName = strdup(pathName);
pNewEntry->handle = handle;
pNewEntry->classLoader = classLoader;
dvmInitMutex(&pNewEntry->onLoadLock);
pthread_cond_init(&pNewEntry->onLoadCond, NULL);
pNewEntry->onLoadThreadId = self->threadId;
/* try to add it to the list */
SharedLib* pActualEntry = addSharedLibEntry(pNewEntry);
if (pNewEntry != pActualEntry) {
ALOGI("WOW: we lost a race to add a shared lib (%s CL=%p)",
pathName, classLoader);
freeSharedLibEntry(pNewEntry);
return checkOnLoadResult(pActualEntry);
}
- If loading is successful, dlsym is used to obtain the JNI_OnLoad function from the corresponding SO file. If this function exists, it is called; otherwise, it returns directly.
else {
if (verbose)
ALOGD("Added shared lib %s %p", pathName, classLoader);
bool result = false;
void* vonLoad;
int version;
vonLoad = dlsym(handle, "JNI_OnLoad");
if (vonLoad == NULL) {
ALOGD("No JNI_OnLoad found in %s %p, skipping init", pathName, classLoader);
result = true;
} else {
/*
* Call JNI_OnLoad. We have to override the current class
* loader, which will always be "null" since the stuff at the
* top of the stack is around Runtime.loadLibrary(). (See
* the comments in the JNI FindClass function.)
*/
OnLoadFunc func = (OnLoadFunc)vonLoad;
Object* prevOverride = self->classLoaderOverride;
self->classLoaderOverride = classLoader;
oldStatus = dvmChangeStatus(self, THREAD_NATIVE);
if (gDvm.verboseJni) {
ALOGI("[Calling JNI_OnLoad for \"%s\"]", pathName);
}
version = (*func)(gDvmJni.jniVm, NULL);
dvmChangeStatus(self, oldStatus);
self->classLoaderOverride = prevOverride;
if (version == JNI_ERR) {
*detail = strdup(StringPrintf("JNI_ERR returned from JNI_OnLoad in \"%s\"",
pathName).c_str());
} else if (dvmIsBadJniVersion(version)) {
*detail = strdup(StringPrintf("Bad JNI version returned from JNI_OnLoad in \"%s\": %d",
pathName, version).c_str());
/*
* It's unwise to call dlclose() here, but we can mark it
* as bad and ensure that future load attempts will fail.
*
* We don't know how far JNI_OnLoad got, so there could
* be some partially-initialized stuff accessible through
* newly-registered native method calls. We could try to
* unregister them, but that doesn't seem worthwhile.
*/
} else {
result = true;
}
if (gDvm.verboseJni) {
ALOGI("[Returned %s from JNI_OnLoad for \"%s\"]",
(result ? "successfully" : "failure"), pathName);
}
}
if (result)
pNewEntry->onLoadResult = kOnLoadOkay;
else
pNewEntry->onLoadResult = kOnLoadFailed;
pNewEntry->onLoadThreadId = 0;
/*
* Broadcast a wakeup to anybody sleeping on the condition variable.
*/
dvmLockMutex(&pNewEntry->onLoadLock);
pthread_cond_broadcast(&pNewEntry->onLoadCond);
dvmUnlockMutex(&pNewEntry->onLoadLock);
return result;
}
}
Summary¶
This indicates that when loading a .so file, functions are executed in the following order (skipped if they do not exist):
- .init function
- Functions in .init_array
- JNI_OnLoad function