Androids中的System.loadLibrary對於依賴so的加載分析

        Android雖然基於Linux系統,但Android本身做了大量的修改。其中對於系統C庫更是自己重新實現——Bionic庫。在源碼目錄構中bionic可以找到相關內容。話題好像偏離有點遠,但System.loadLibrary最終還是調用到bionic庫中的函數dlopen。這個dlopen搜索依賴so的路徑與Linux本身有着不一樣的實現,並且在4.2.2版本及以下有着無法隱式加載app目錄下依賴so的缺陷,直到4.3才被修復。

        關於System.loadLibrary和System.load兩個在java中加載so的方法,已經有人進行個大致的分析(Java中System.loadLibrary() 的執行過程  文章中的分析內容是4.2.2版本,仍然不支持隱式加載app目錄下依賴so),可以先看看這篇文章裏關於以上兩個java函數的實現,瞭解大部分的流程。然後,這裏就要開始從dlopen開始分析,對於依賴so加載的問題。


具體分析

        首先來看這樣一個例子,有兩個so,liba.so、libb.so。其中,liba.so引用了libb.so導出的符號,並且是直接引用符號,也就是

需要加載器在運行時的動態隱式鏈接。在加載liba.so的時候,Java層裏,我們會調用System.loadLibrary(),或者System.load()來

指定so的路徑,然後最終會調用到native代碼中,使用dlopen加載liba.so,就會根據liba.so的Dynamic section中列出類型爲

DT_NEEDED的依賴共享庫so,並且會遞歸地一個一個地加載依賴so。在LInux下,加載這些依賴so的路徑會是 1. 環境變量LD_LIBRARY指明的路徑。2. /etc/ld.so.cache中的函數庫列表;3/lib目錄,然後/usr/lib。但在4.2.2版本的Android的Bionic實現中,加載的目錄只有LD_LIBRARY_PATH變量所指示的路徑以及被hardcode進代碼的/system/lib,/vendor/lib這兩個路徑。而事實上LD_LIBRARY_PATH是不建議修改的,而且默認值在Android中也是/system/lib,/vendor/lib這兩個路徑,因此事實上,被依賴的so加載路徑只有/system/lib,/vendor/lib這兩個!

        不過這種問題在4.3開始得到改正。在4.3版本的代碼裏,nativeLoad這個方法接受多一個參數ldLibraryPath,這是通過PathClassLoader獲取App的native lib路徑,然後傳給nativeLoad的。nativeLoad在獲取這個參數之後,就會調用bionic的方法來顯式更新內部的ldPath路徑,這樣每次加載的時候,就可以先搜索本地App的native lib路徑,就能夠正確加載依賴的so。

首先我們來看看4.3版本的實現:

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);
        }
    }
        doLoad方法在System.loadLibrary裏被調用,裏面的英文註釋可以看到這次4.3的改進機制以及原因,主要是通過classloader把native lib的路徑傳入到bionic中,然後每次加載依賴so都會先從這些路徑開始搜索。另外,PathClassLoader的構造是在加載apk的時候,通過傳遞nativeLibraryDir來初始化的。

static void Dalvik_java_lang_Runtime_nativeLoad(const u4* args, JValue* pResult)
{
    //......
    StringObject* ldLibraryPathObj = (StringObject*) args[2];
    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);
    }
    //......
}

可以看到nativeLoad方法會通過顯式獲取bionic庫中的android_update_LD_LIBRARY_PATH方法,然後這個路徑最終會更新bionic庫的全局變量gLdPaths,這個變量在加載依賴so的時候會產生作用。

static int open_library_on_path(const char* name, const char* const paths[]) {
  char buf[512];
  for (size_t i = 0; paths[i] != NULL; ++i) {
    int n = __libc_format_buffer(buf, sizeof(buf), "%s/%s", paths[i], name);
    if (n < 0 || n >= static_cast<int>(sizeof(buf))) {
      PRINT("Warning: ignoring very long library path: %s/%s", paths[i], name);
      continue;
    }
    int fd = TEMP_FAILURE_RETRY(open(buf, O_RDONLY | O_CLOEXEC));
    if (fd != -1) {
      return fd;
    }
  }
  return -1;
}

static int open_library(const char* name) {
  TRACE("[ opening %s ]", name);

  // If the name contains a slash, we should attempt to open it directly and not search the paths.
  if (strchr(name, '/') != NULL) {
    int fd = TEMP_FAILURE_RETRY(open(name, O_RDONLY | O_CLOEXEC));
    if (fd != -1) {
      return fd;
    }
    // ...but nvidia binary blobs (at least) rely on this behavior, so fall through for now.
  }

  // Otherwise we try LD_LIBRARY_PATH first, and fall back to the built-in well known paths.
  int fd = open_library_on_path(name, gLdPaths);
  if (fd == -1) {
    fd = open_library_on_path(name, gSoPaths);
  }
  return fd;
}
        可以看到,open_library會調用gLdPaths裏面的路徑來加載依賴的so。另外gSoPaths這個全局變量被初始化爲/system/lib,/vendor/lib這兩個系統路徑。

在4.3以下的版本里,gLdPaths的初始化是在linker的初始化裏通過讀取LD_LIBRARY_PATH來決定的,並不是像4.3這樣可以每次動態更新。具體實現如下:

/* skip past the environment */
    while(vecs[0] != 0) {
        if(!strncmp((char*) vecs[0], "DEBUG=", 6)) {
            debug_verbosity = atoi(((char*) vecs[0]) + 6);
        } else if(!strncmp((char*) vecs[0], "LD_LIBRARY_PATH=", 16)) {
            ldpath_env = (char*) vecs[0] + 16;
        }
        vecs++;
    }
  //......
 
        /* Use LD_LIBRARY_PATH if we aren't setuid/setgid */
    if (ldpath_env && getuid() == geteuid() && getgid() == getegid())
        parse_library_path(ldpath_env, ":");

        以上這段代碼是在__linker_init函數裏的代碼片段,parse_library_path函數的作用就是設置加載依賴so的路徑。另外,關於至於如何解析so裏的dynamic section以及如何加載,並且初始化so等這裏不屬於本次討論範疇,就不再贅述了。


解決辦法:

        由於加載依賴so的路徑在4.3版本以前會存在問題(即調用System.loadLibrary("a");會失敗),所以我們只能夠在顯式加載每個依賴最低的庫,這樣最後加載依賴最高的so就會成功。根據上面的例子,我們可以先顯式加載libb.so,然後再加載liba.so。代碼如下:

System.loadLibrary("b");
System.loadLibrary("a"); 
這樣就可以成功加載兩個so。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章