Android 分析Native庫的載入過程及x86系統執行arm庫的原理
本文主要講述Android 載入動態連結庫的過程,為了分析工作中遇到的一個問題 x86的系統是如何執行arm的動態連結庫的。
參考部落格:
https://pqpo.me/2017/05/31/system-loadlibrary/ 深入理解 System.loadLibrary
https://www.jianshu.com/p/bf8b4a90f825 Android Native庫的載入及動態連結
https://blog.csdn.net/groundhappy/article/details/80493358 android的native_bridge
基於android7.0程式碼,涉及檔案:
libcore\ojluni \src\main\java\java\lang\System.java
libcore\ojluni\src\main\java\java\lang\Runtime.java
libcore\dalvik\src\main\java\dalvik\system\PathClassLoader.java
libcore\dalvik\src\main\java\dalvik\system\DexClassLoader.java
libcore\ojluni\src\main\native\Runtime.c
art\runtime\openjdkjvm\OpenjdkJvm.cc
art\runtime \java_vm_ext.cc
system\core\libnativeloader\native_loader.cpp
bionic\linker\dlfcn.cpp
bionic\linker\linker.cpp
android是基於linux系統的,在開始前像看下Linux系統下是如何載入動態連結庫有助於理解Android的動態庫載入流程。
Linux環境下載入動態庫主要包括如下函式,位於標頭檔案dlfcn.h中:同樣android的函式也位於dlfcn.h標頭檔案中。
void *dlopen(const char *filename, int flag); //開啟動態連結庫
char *dlerror(void); //獲取錯誤資訊
void *dlsym(void *handle, const char *symbol); //獲取方法指標
int dlclose(void *handle); //關閉動態連結庫
用一個簡單的C++程式碼,作為動態連結庫包含計算相關的函式:(懶 使用的參考文章中demo)
extern "C"
int add(int a, int b) {
return a + b;
}
extern "C"
int mul(int a, int b) {
return a*b;
}
extern “C” 表示告訴編譯器以C的方式編譯,不要修改函式名,否則C++會修改函式名。
然後通過下述命令編譯成動態連結庫:
g++ -fPIC -shared caculate.cpp -o libcaculate.so
這樣會在同級目錄下生成一個動態庫檔案:libcaculate.so
然後編寫載入動態庫並使用的程式碼:
[main_call.cpp]
#include <iostream>
#include <dlfcn.h>
using namespace std;
static const char * const LIB_PATH = "./libcaculate.so";
typedef int (*CACULATE_FUNC)(int, int);
int main() {
void* symAdd = nullptr;
void* symMul = nullptr;
char* errorMsg = nullptr;
dlerror();
//1.開啟動態庫,拿到一個動態庫控制代碼
void* handle = dlopen(LIB_PATH, RTLD_NOW);
if(handle == nullptr) {
cout << "load error!" << endl;
return -1;
}
// 檢視是否有錯誤
if ((errorMsg = dlerror()) != nullptr) {
cout << "errorMsg:" << errorMsg << endl;
return -1;
}
cout << "load success!" << endl;
//2.通過控制代碼和方法名獲取方法指標地址
symAdd = dlsym(handle, "add");
if(symAdd == nullptr) {
cout << "dlsym failed!" << endl;
if ((errorMsg = dlerror()) != nullptr) {
cout << "error message:" << errorMsg << endl;
return -1;
}
}
//3.將方法地址強制型別轉換成方法指標
CACULATE_FUNC addFunc = reinterpret_cast(symAdd);
//4.呼叫動態庫中的方法
cout << "1 + 2 = " << addFunc(1, 2) << endl;
//5.通過控制代碼關閉動態庫
dlclose(handle);
return 0;
}
主要就用到了上面的4個函式過程如下
1、開啟動態庫,拿到一個動態庫控制代碼
2、通過控制代碼和方法名獲取方法指標地址
3、將方法地址強制型別轉換成方法指標
4、呼叫動態庫中的方法
5、通過控制代碼關閉動態庫。
中間會使用dlerror檢測是否有錯誤。
有必要解釋一下的是方法指標地址到方法指標的轉換,為了方便這裡定義了一個方法指標的別名:
typedef int (*CACULATE_FUNC)(int, int);
指明該方法接受兩個int型別引數返回一個int值。
拿到地址之後強制型別轉換成方法指標用於呼叫:
CACULATE_FUNC addFunc = reinterpret_cast(symAdd);
最後只要編譯執行即可:
g++ -std=c++11 -ldl main_call.cpp -o main
.main
因為程式碼中使用了c++11標準新加的特性,所以編譯的時候帶上-std=c++11,另外使用了標頭檔案dlfcn.h需要帶上-ldl,編譯生成的main檔案即是二進位制可執行檔案,需要將動態庫放在同級目錄下執行。
上面就是Linux環境下建立動態庫,載入並使用動態庫的全部過程。
由於Android基於Linux系統,所有Android系統底層也是通過這種方式載入並使用動態庫的。
Android 連結器Linker之前的工作
流程圖來自參考的另一篇部落格
下面從System.loadLibrary() 開始分析
public static void loadLibrary(String libname) {
//VMStack.getCallingClassLoader() 返回應用類載入器這裡是:PathClassLoader
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
下面看* loadLibrary0()*
synchronized void loadLibrary0(ClassLoader loader, String libname) {
if (libname.indexOf((int)File.separatorChar) != -1) {
throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
}
String libraryName = libname;
if (loader != null) {
//findLibrary()返回的是庫的全路徑名,loader是PathClassLoader 最終會
//呼叫父類的findLibrary()方法。
String filename = loader.findLibrary(libraryName);
//這裡可以通過Logger 來列印log 因為這時候 util.log是無法執行到這裡
Logger logger = Logger.getLogger("lly");
logger.info("filename == "+filename);
logger.info("mapLibraryName == "+System.mapLibraryName(libraryName));
if (filename == null) {
// It's not necessarily true that the ClassLoader used
// System.mapLibraryName, but the default setup does, and it's
// misleading to say we didn't find "libMyLibrary.so" when we
// actually searched for "liblibMyLibrary.so.so".
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\"");
}
//裝載動態庫
String error = doLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
......
}
引數loader為Android的應用類載入器,它是PathClassLoader 型別的物件,繼承自BaseDexClassLoader物件
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
最終會呼叫DexPathList 的findLibrary()方法
public String findLibrary(String libraryName) {
//生成平臺相關的庫名稱這裡會返回libxxx.so
String fileName = System.mapLibraryName(libraryName);
for (Element element : nativeLibraryPathElements) {
//查詢動態庫返回的全路徑名
String path = element.findNativeLibrary(fileName);
if (path != null) {
return path;
}
}
return null;
}
回到loadLibrary0(),有了動態庫的全路徑名就可以裝載庫了,下面看doLoad()。
private String doLoad(String name, ClassLoader loader) {
String librarySearchPath = null;
if (loader != null && loader instanceof BaseDexClassLoader) {
BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader;
librarySearchPath = dexClassLoader.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, librarySearchPath);
}
}
nativeLoad最終呼叫Runtime.c中的Runtime_nativeLoad(),接著呼叫OpenjdkJvm.cc 中的 JVM_NativeLoad() ,最終會呼叫到 Java_vm_ext.cc 中的LoadNativeLibrary() so載入的過程主要在這個函式中完成,參照上面的Linux載入so的流程,我們分析下這個方法:
bool JavaVMExt::LoadNativeLibrary(JNIEnv* env,
const std::string& path,
jobject class_loader,
jstring library_path,
std::string* error_msg){
//1、開啟動態連結庫
void* handle = android::OpenNativeLibrary(env,
runtime_->GetTargetSdkVersion(),
path_str,
class_loader,
library_path);
//這裡是x86為相容arm庫檔案採用的方案 使用houdini技術,在執行時動態轉化指令集,從而實現對arm庫的支援。
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;
}
if (env->ExceptionCheck() == JNI_TRUE) {
LOG(ERROR) << "Unexpected exception:";
env->ExceptionDescribe();
env->ExceptionClear();
}
// Create a new entry.
// TODO: move the locking (and more of this logic) into Libraries.
bool created_library = false;
{
// Create SharedLibrary ahead of taking the libraries lock to maintain lock ordering.
std::unique_ptr<SharedLibrary> new_library(
new SharedLibrary(env, self, path, handle, class_loader, class_loader_allocator));
MutexLock mu(self, *Locks::jni_libraries_lock_);
library = libraries_->Get(path);
if (library == nullptr) { // We won race to get libraries_lock.
library = new_library.release();
libraries_->Put(path, library);
created_library = true;
}
}
if (!created_library) {
LOG(INFO) << "WOW: we lost a race to add shared library: "
<< "\"" << path << "\" ClassLoader=" << class_loader;
return library->CheckOnLoadResult();
}
VLOG(jni) << "[Added shared library \"" << path << "\" for ClassLoader " << class_loader << "]";
bool was_successful = false;
void* sym;
if (needs_native_bridge) {
library->SetNeedsNativeBridge();
}
//2、獲取方法地址
sym = library->FindSymbol("JNI_OnLoad", nullptr);
if (sym == nullptr) {
VLOG(jni) << "[No JNI_OnLoad found in \"" << path << "\"]";
was_successful = 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.)
ScopedLocalRef<jobject> old_class_loader(env, env->NewLocalRef(self->GetClassLoaderOverride()));
self->SetClassLoaderOverride(class_loader);
VLOG(jni) << "[Calling JNI_OnLoad in \"" << path << "\"]";
typedef int (*JNI_OnLoadFn)(JavaVM*, void*);
//3、強制型別轉換成函式指標
JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
//4、呼叫函式
int version = (*jni_on_load)(this, nullptr);
......
library->SetResult(was_successful);
return was_successful;
}
arm的so可以執行在x86的系統上原因就是因為這個分支:
if (handle == nullptr) {
if (android::NativeBridgeIsSupported(path_str)) {
handle = android::NativeBridgeLoadLibrary(path_str, RTLD_NOW);
needs_native_bridge = true;
}
}
當呼叫OpenNativeLibrary()開啟so時會去讀取so檔案的資訊,x86的標頭檔案和arm的標頭檔案資訊是不一樣的,所有在用x86的的手機上執行arm的so檔案時handle為空,這時候會根據so檔案的絕對路徑來判斷是否支援houdini,如果支援的話會用NativeBridgeLoadLibrary()
重新開啟so檔案,進行下一步操作。測試已知支援arm so檔案的路徑有:
/data/app/包名/lib/arm/libxxx.so
/system/priv-app/應用名稱/lib/arm/libxxx.so
到這裡其實我的問題已經解決了,關於為什麼會去這些路徑下找,由於Native Bridge不開源,是以so的方式提供的,沒有辦法跟進去,望知道的分享一下。
下面看下Android 連結器Linker的裝載過程
其中會在load_library 讀取ELF檔案頭以及一些段資訊
static bool load_library(android_namespace_t* ns,
LoadTask* task,
LoadTaskList* load_tasks,
int rtld_flags,
const std::string& realpath){
......
if (!task->read(realpath.c_str(), file_stat.st_size)) {
soinfo_free(si);
task->set_soinfo(nullptr);
return false;
}
.......
return true;
}
看下Read方法
bool ElfReader::Read(const char* name, int fd, off64_t file_offset, off64_t file_size) {
CHECK(!did_read_);
CHECK(!did_load_);
name_ = name;
fd_ = fd;
file_offset_ = file_offset;
file_size_ = file_size;
if (ReadElfHeader() &&
VerifyElfHeader() &&
ReadProgramHeaders() &&
ReadSectionHeaders() &&
ReadDynamicSection()) {
did_read_ = true;
}
__libc_format_log(ANDROID_LOG_DEBUG, "lly", "did_read_ == %d",did_read_);
return did_read_;
}
ReadElfHeader() : 讀取ELF檔案頭資訊
VerifyElfHeader() : 校驗ELF(檔案型別等)
ReadProgramHeaders() : 根據ELF檔案頭資訊獲取程式頭表
ReadSectionHeaders() : 根據ELF檔案頭資訊獲取段頭表
ReadDynamicSection() : 獲取Dynamic Section的資訊
常見的 has unexpected e_machine: 40 就是在 VerifyElfHeader()方法中提示的。
最後看下Native庫的動態連結過程: