Android原始碼解析之loadLibrary() 的執行過程
轉載自:http://gityuan.com/2017/03/26/load_library/
重要參照(可惜格式原因,不然我想轉這篇,該篇4.2.2):https://my.oschina.net/wolfcs/blog/129696
我想抱怨一句,為啥都快底層的東西了,都還老是變來變去啊……
本文講述的Android 6.0系統體系架構, 分析動態庫的載入過程.
libcore/luni/src/main/java/java/lang/System.java
libcore/luni/src/main/java/java/lang/Runtime.java
libcore/luni/src/main/native/java_lang_System.cpp
bionic/linker/linker.cpp
art/runtime/native /java_lang_Runtime.cc
art/runtime/java_vm_ext.cc
一. 概述
動態庫操作,所需要的標頭檔案的#include, 最為核心的方法如下:
void *dlopen(const char * pathname,int mode); //開啟動態庫
void *dlsym(void *handle,const char *name); //獲取動態庫物件地址
char *dlerror(vid); //錯誤檢測
int dlclose(void * handle); //關閉動態庫
而對於android上層的Java程式碼來說,都封裝好了, 只需要一行程式碼就即可完成動態庫的載入過程,如下:
System.loadLibrary("gityuan_jni");
接下來,解析這行程式碼背後的故事.
二. 動態庫載入過程
2.1 System.loadLibrary
[-> System.java]
public static void loadLibrary(String libName) {
Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
}
此處getCallingClassLoader返回的是呼叫者所定義的ClassLoader.
2.2 Runtime.loadLibrary
[-> Runtime.java]
void loadLibrary(String libraryName, ClassLoader loader) {
if (loader != null) {
//[見小節2.4]
String filename = loader.findLibrary(libraryName);
if (filename == null) {
throw new UnsatisfiedLinkError(...);
}
//成功執行完doLoad,則返回.[見小節2.5]
String error = doLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
//當loader為空的情況下執行[見2.3]
String filename = System.mapLibraryName(libraryName);
List<String> candidates = new ArrayList<String>();
String lastError = null;
//此處的mLibPaths取值 [見小節三]
for (String directory : mLibPaths) {
String candidate = directory + filename;
candidates.add(candidate);
if (IoUtils.canOpenReadOnly(candidate)) {
String error = doLoad(candidate, loader);
if (error == null) {
return; //成功執行完doLoad,則返回.
}
lastError = error;
}
}
if (lastError != null) {
throw new UnsatisfiedLinkError(lastError);
}
throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
}
載入庫的兩條路徑,如下:
- 當loader不為空時, 通過該loader來findLibrary()檢視目標庫所在路徑;
- 當loader為空時, 則從預設目錄mLibPaths下來查詢是否存在該動態庫;
不管loader是否為空, 找到目標庫所在路徑後,都會呼叫doLoad來真正用於載入動態庫.
2.3 loader為空的情況
[-> System.java]
public static String mapLibraryName(String nickname) {
if (nickname == null) {
throw new NullPointerException("nickname == null");
}
return "lib" + nickname + ".so";
}
該方法的功能是將xxx動態庫的名字轉換為libxxx.so. 比如前面傳遞過來的nickname為gityuan_jni, 經過該方法處理後返回的名字為libgityuan_jni.so.
mLibPaths取值分兩種情況:
- 對於64系統,則為/system/lib64和/vendor/lib64;
- 對於32系統,則為/system/lib和/vendor/lib.
假設此處為64位系統, 則會去查詢/system/lib64/libgityuan_jni.so或/vendor/lib64/libgityuan_jni.so庫是否存在.
絕大多數的動態庫都在/system/lib64或/system/lib路徑下.通過canOpenReadOnly方法來判定目標動態庫是否存在, 找到則直接返回,否則丟擲UnsatisfiedLinkError異常.
2.4 loader不為空的情況
ClassLoader一般來說都是PathClassLoader, 這就不再解釋. 從該物件的findLibrary說起. 由於PathClassLoader繼承於 BaseDexClassLoader物件, 並且沒有覆寫該方法, 故呼叫其父類所對應的方法.
2.4.1 findLibrary
[-> BaseDexClassLoader.java]
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
super(parent);
//dexPath一般是指apk所在路徑
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
public String findLibrary(String name) {
return pathList.findLibrary(name); //[見小節2.4.3]
}
}
2.4.2 DexPathList
[-> DexPathList.java]
public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {
...
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
//記錄所有的dexFile檔案
this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);
//app目錄的native庫
this.nativeLibraryDirectories = splitPaths(libraryPath, false);
//系統目錄的native庫
this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true);
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
//記錄所有的Native動態庫
this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories, null,
suppressedExceptions);
...
}
DexPathList初始化過程,主要功能是收集以下兩個變數資訊:
- dexElements: 記錄所有的dexFile檔案
- nativeLibraryPathElements: 記錄所有的Native動態庫, 包括app目錄的native庫和系統目錄的native庫.
接下來便是從pathList中查詢目標動態庫.
2.4.3 findLibrary
[-> DexPathList.java]
public String findLibrary(String libraryName) {
String fileName = System.mapLibraryName(libraryName);
for (Element element : nativeLibraryPathElements) {
//[見小節2.4.4]
String path = element.findNativeLibrary(fileName);
if (path != null) {
return path;
}
}
return null;
}
gityuan_jni同樣也是經過mapLibraryName(), 處理後得到的名字為libgityuan_jni.so. 接下來,從所有的動態庫nativeLibraryPathElements(包含兩個系統路徑)查詢是否存在匹配的. 這裡舉例說明, 一般地會在以下兩個路徑查詢(以64位為例):
- /data/app/com.gityuan.blog-1/lib/arm64
- /vendor/lib64
- /system/lib64
2.4.4 findNativeLibrary
[-> DexPathList.java ::Element]
final class DexPathList {
static class Element {
public String findNativeLibrary(String name) {
maybeInit();
if (isDirectory) {
String path = new File(dir, name).getPath();
if (IoUtils.canOpenReadOnly(path)) {
return path;
}
} else if (zipFile != null) {
String entryName = new File(dir, name).getPath();
if (isZipEntryExistsAndStored(zipFile, entryName)) {
return zip.getPath() + zipSeparator + entryName;
}
}
return null;
}
}
}
遍歷查詢,一旦找到則返回所找到的目標動態庫. 接下來, 再回到[小節2.2]來看看動態庫的載入doLoad:
2.5 Runtime.doLoad
[-> Runtime.java]
private String doLoad(String name, ClassLoader loader) {
String ldLibraryPath = null;
String dexPath = null;
if (loader == null) {
ldLibraryPath = System.getProperty("java.library.path");
} else if (loader instanceof BaseDexClassLoader) {
BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader;
ldLibraryPath = dexClassLoader.getLdLibraryPath();
}
synchronized (this) {
//[見小節2.5.1]
return nativeLoad(name, loader, ldLibraryPath);
}
}
此處ldLibraryPath有兩種情況:
- 當loader為空, 則ldLibraryPath為系統目錄下的Native庫;
- 當lodder不為空, 則ldLibraryPath為app目錄下的Native庫;
接下來, 繼續看看nativeLoad.
2.5.1 nativeLoad
[-> java_lang_Runtime.cc]
static jstring Runtime_nativeLoad(JNIEnv* env, jclass, jstring javaFilename, jobject javaLoader,
jstring javaLdLibraryPathJstr) {
ScopedUtfChars filename(env, javaFilename);
if (filename.c_str() == nullptr) {
return nullptr;
}
SetLdLibraryPath(env, javaLdLibraryPathJstr);
std::string error_msg;
{
JavaVMExt* vm = Runtime::Current()->GetJavaVM();
//[見小節2.5.2]
bool success = vm->LoadNativeLibrary(env, filename.c_str(), javaLoader, &error_msg);
if (success) {
return nullptr;
}
}
env->ExceptionClear();
return env->NewStringUTF(error_msg.c_str());
}
nativeLoad方法經過jni,進入該方法執行.
2.5.2 LoadNativeLibrary
[-> java_vm_ext.cc]
bool JavaVMExt::LoadNativeLibrary(JNIEnv* env, const std::string& path, jobject class_loader,
std::string* error_msg) {
error_msg->clear();
SharedLibrary* library;
Thread* self = Thread::Current();
{
MutexLock mu(self, *Locks::jni_libraries_lock_);
library = libraries_->Get(path); //檢查該動態庫是否已載入
}
if (library != nullptr) {
if (env->IsSameObject(library->GetClassLoader(), class_loader) == JNI_FALSE) {
//不能載入同一個採用多個不同的ClassLoader
return false;
}
...
return true;
}
const char* path_str = path.empty() ? nullptr : path.c_str();
//通過dlopen開啟動態共享庫.該庫不會立刻被解除安裝直到引用技術為空.
void* handle = dlopen(path_str, RTLD_NOW);
bool needs_native_bridge = false;
if (handle == nullptr) {
if (android::NativeBridgeIsSupported(path_str)) {
handle = android::NativeBridgeLoadLibrary(path_str, RTLD_NOW);
needs_native_bridge = true;
}
}
if (handle == nullptr) {
*error_msg = dlerror(); //開啟失敗
VLOG(jni) << "dlopen(\"" << path << "\", RTLD_NOW) failed: " << *error_msg;
return false;
}
bool created_library = false;
{
std::unique_ptr<SharedLibrary> new_library(
new SharedLibrary(env, self, path, handle, class_loader));
MutexLock mu(self, *Locks::jni_libraries_lock_);
library = libraries_->Get(path);
if (library == nullptr) {
library = new_library.release();
//建立共享庫,並新增到列表
libraries_->Put(path, library);
created_library = true;
}
}
...
bool was_successful = false;
void* sym;
//查詢JNI_OnLoad符號所對應的方法
if (needs_native_bridge) {
library->SetNeedsNativeBridge();
sym = library->FindSymbolWithNativeBridge("JNI_OnLoad", nullptr);
} else {
sym = dlsym(handle, "JNI_OnLoad");
}
if (sym == nullptr) {
was_successful = true;
} else {
//需要先覆蓋當前ClassLoader.
ScopedLocalRef<jobject> old_class_loader(env, env->NewLocalRef(self->GetClassLoaderOverride()));
self->SetClassLoaderOverride(class_loader);
typedef int (*JNI_OnLoadFn)(JavaVM*, void*);
JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
// 真正呼叫JNI_OnLoad()方法的過程
int version = (*jni_on_load)(this, nullptr);
if (runtime_->GetTargetSdkVersion() != 0 && runtime_->GetTargetSdkVersion() <= 21) {
fault_manager.EnsureArtActionInFrontOfSignalChain();
}
//執行完成後, 需要恢復到原來的ClassLoader
self->SetClassLoaderOverride(old_class_loader.get());
...
}
library->SetResult(was_successful);
return was_successful;
}
該過程簡單總結:
- 檢查該動態庫是否已載入;
- 通過dlopen開啟動態共享庫;
- 建立SharedLibrary共享庫,並新增到libraries_列表;
- 找到JNI_OnLoad符號所對應的方法, 並呼叫該方法.
三. mLibPaths初始化
mLibPaths的初始化過程, 要從libcore/luni/src/main/java/java/lang/System.java類的靜態程式碼塊初始化開始說起. 對於類的靜態程式碼塊,編譯過程會將所有的靜態程式碼塊和靜態成員變數的賦值過程都收集整合到clinit方法, 即類的初始化方法.如下:
public final class System {
static {
...
//[見小節3.1]
unchangeableSystemProperties = initUnchangeableSystemProperties();
systemProperties = createSystemProperties();
addLegacyLocaleSystemProperties();
}
}
3.1 initUnchangeableSystemProperties
[-> System.java]
private static Properties initUnchangeableSystemProperties() {
VMRuntime runtime = VMRuntime.getRuntime();
Properties p = new Properties();
...
p.put("java.boot.class.path", runtime.bootClassPath());
p.put("java.class.path", runtime.classPath());
// [見小節3.2]
parsePropertyAssignments(p, specialProperties());
...
return p;
}
這個過程會將大量的key-value對儲存到Properties物件, 這種重點看specialProperties
3.1.1 parsePropertyAssignments
[-> System.java]
private static void parsePropertyAssignments(Properties p, String[] assignments) {
for (String assignment : assignments) {
int split = assignment.indexOf('=');
String key = assignment.substring(0, split);
String value = assignment.substring(split + 1);
p.put(key, value);
}
}
將assignments資料解析後儲存到Properties物件.
3.2 specialProperties
[-> java_lang_System.cpp]
static jobjectArray System_specialProperties(JNIEnv* env, jclass) {
std::vector<std::string> properties;
char path[PATH_MAX];
...
const char* library_path = getenv("LD_LIBRARY_PATH");
if (library_path == NULL) {
// [見小節3.3]
android_get_LD_LIBRARY_PATH(path, sizeof(path));
library_path = path;
}
properties.push_back(std::string("java.library.path=") + library_path);
return toStringArray(env, properties);
}
環境變數LD_LIBRARY_PATH為空, 可通過adb shell env命令來檢視環境變數的值. 接下來進入android_get_LD_LIBRARY_PATH方法, 該方法呼叫do_android_get_LD_LIBRARY_PATH,見下文:
3.3 do_android_get_LD_LIBRARY_PATH
[-> linker.cpp]
void do_android_get_LD_LIBRARY_PATH(char* buffer, size_t buffer_size) {
//[見小節3.4]
size_t required_len = strlen(kDefaultLdPaths[0]) + strlen(kDefaultLdPaths[1]) + 2;
char* end = stpcpy(buffer, kDefaultLdPaths[0]);
*end = ':';
strcpy(end + 1, kDefaultLdPaths[1]);
}
可見賦值過程還得看kDefaultLdPaths陣列.
3.4 kDefaultLdPaths
[-> linker.cpp]
static const char* const kDefaultLdPaths[] = {
#if defined(__LP64__)
"/vendor/lib64",
"/system/lib64",
#else
"/vendor/lib",
"/system/lib",
#endif
nullptr
};
對於linux 64位作業系統則定義__LP64__巨集, 可知mLibPaths取值分兩種情況:
- 對於64系統,則為/system/lib64和/vendor/lib64;
- 對於32系統,則為/system/lib和/vendor/lib.
也可以通過System.getProperty(“java.library.path”)來獲取該值.