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。



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