1. 程式人生 > >Android 分析Native庫的載入過程及x86系統執行arm庫的原理

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庫的動態連結過程:
這裡寫圖片描述