android 加载so过程分析

2023-11-22 18:32:41 浏览数 (1)

在实现android插件化过程中,在插件代码中加载so时出现了一些问题,因此特地研究了一下android系统中加载so的过程,记录下来,整理成文。

在android系统中,加载so一般会调用System.loadLibrary(name)或者是System.load(path),这两个函数都可以用来加载so文件,区别在于System.loadLibrary函数的参数为库文件名,而System.load函数的 参数为库文件的绝对路径,可以是任意路径(路径需要可执行权限)。这两个函数本质上都是一样的,只是搜索so的搜索目录略有差别。下面以System.loadLibrary函数为例来分析加载so的实现原理。

首先看一下System.loadLibrary函数的源码(ibcore/luni/src/main/java/java/lang/System.java):

代码语言:javascript复制
   public static void loadLibrary(String libName) {
        SecurityManager smngr = System.getSecurityManager();
        if (smngr != null) {
            smngr.checkLink(libName);
        }
        Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
    }

如上所示,通过VMStack拿到了当前的ClassLoader,然后将加载so的动作委托给Runtime.loadLibrary去执行(libcore/luni/src/main/java/java/lang/Runtime.java):

代码语言:javascript复制
 void loadLibrary(String libname, ClassLoader loader) {
        String filename;
        int i;

        if (loader != null) {
            filename = loader.findLibrary(libname);
            if (filename != null && nativeLoad(filename, loader))
                return;
            // else fall through to exception
        } else {
            filename = System.mapLibraryName(libname);
            for (i = 0; i < mLibPaths.length; i  ) {
                if (false)
                    System.out.println("Trying "   mLibPaths[i]   filename);
                if (nativeLoad(mLibPaths[i]   filename, loader))
                    return;
            }
        }

        throw new UnsatisfiedLinkError("Library "   libname   " not found");

如上图所示,正常情况下Classloader不会为空,因此进入第一个if分支,可以看看出在此函数中完成了两件事:

1:通过调用ClassLoader.findLibrary函数去拿到so的真正的文件路径;

2:调用nativeLoad函数去实现真正的so加载;

这里会牵扯到一个问题,如何通过so的名称去ClassLoader拿到so真正的文件路径?Android系统中可供使用的ClassLoader有两个,分别是DexClassLoader和PathClassLoader,其中PathClassLoader一般用于加载已经安装过的系统app的dex文件,而DexClassLoader可以加载任意路径的apk/jar文件(此文件路径需要可执行权限),两者间的具体差别请参考developer.android.com。DexClassLoader和PathClassLoader对于findLibrary函数的实现大致相同,下面来看看PathClassLoader中findLibrary函数的实现(libcore/luni/src/main/java/java/PathClassLoader.java):

代码语言:javascript复制
protected String findLibrary(String libname) {
        ensureInit();

        String fileName = System.mapLibraryName(libname);
        for (int i = 0; i < mLibPaths.length; i  ) {
            String pathName = mLibPaths[i]   fileName;
            File test = new File(pathName);

            if (test.exists())
                return pathName;
        }

        return null;
    }

如上所示,首先调用System.mapLibraryName拿到so的前缀和后缀名,如libname为hello,那么经过此函数转换后变成了libhello.so,然后在mLibPaths搜索目录下搜寻libhello.so文件,如果文件存在,则代表找到了此so的文件,直接返回即可。mLibPaths是如何初始化的呢?还有如上所示,在mLibPaths搜索目录下搜寻是有序的,只要搜索到了就立即返回,因此如果在mLibPaths[0]和mLibPaths[1]目录下均有这个so,会优先返回mLibPaths[0]目录下so的文件路径的,因此我们也需要关注mLibPaths中搜索目录的顺序。

在findLibrary函数开始调用ensureInit函数后会初始化mLibPaths搜索目录,下面看看这个函数的具体实现(主要关注mLibPaths中搜索目录的内容和顺序,以下代码省略了部分无关代码)。

代码语言:javascript复制
 private synchronized void ensureInit() {
        if (initialized) {
            return;
        }
        
        initialized = true;
        
        /*
         * Prep for native library loading.
         */
        String pathList = System.getProperty("java.library.path", ".");
        String pathSep = System.getProperty("path.separator", ":");
        String fileSep = System.getProperty("file.separator", "/");
        
        if (libPath != null) {
            if (pathList.length() > 0) {
                pathList  = pathSep   libPath;
            }
            else {
                pathList = libPath;
            }
        }

        mLibPaths = pathList.split(pathSep);
        length = mLibPaths.length;

        // Add a '/' to the end so we don't have to do the property lookup
        // and concatenation later.
        for (int i = 0; i < length; i  ) {
            if (!mLibPaths[i].endsWith(fileSep))
                mLibPaths[i]  = fileSep;
            if (false)
                System.out.println("Native lib path:  "   mLibPaths[i]);
        }
    }

这段代码看上去挺简单,主要是从系统获取到"java.library.path"属性,libPath为应用程序的搜索目录,libPath是在构造PathClassLoader时由系统传进来的(一般不会为空),如果libPath不为空,则添加到mLibPaths,由代码可以确定搜索目录的顺序是系统的搜索目录优先,应用程序的搜索目录在最后。

总结一下,ClassLoader的findLibrary实际上会去两部分目录下搜索so,一部分是通过System.getProperty("java.library.path", ".")拿到的系统搜索目录,还有部分是在构造PathClassLoader时传进来的librarypath。在三星手机上,mLibPaths分别如下:

1:/vendor/lib

2:/system/lib

3:/data/data/应用包名/lib

每个手机可能根据系统的不同而有不同的应用程序搜索目录,如在有些手机上应用程序的搜索目录为/data/app-lib/.apk名称 目录下(可以参考PackageManagerService中部分代码);

解决了ClassLoader.findLibrary函数的问题,现在去看看nativeLoad函数的实现。nativeLoad是native函数,真正的实现位于/android_dalvik-eclair/vm//native/java_lang_Runtime.c:

代码语言:javascript复制
static void Dalvik_java_lang_Runtime_nativeLoad(const u4* args,
    JValue* pResult)
{
    StringObject* fileNameObj = (StringObject*) args[0];
    Object* classLoader = (Object*) args[1];
    char* fileName;
    int result;

    if (fileNameObj == NULL)
        RETURN_INT(false);
    fileName = dvmCreateCstrFromString(fileNameObj);

    result = dvmLoadNativeCode(fileName, classLoader);

    free(fileName);
    RETURN_INT(result);
}

可以看到,nativeLoad()实际上只是完成了两件事情,第一,是调用dvmCreateCstrFromString()将Java 的library path String 转换到native的String,然后将这个path传给dvmLoadNativeCode()做load,dvmLoadNativeCode()这个函数的实现在/android_dalvik-eclair/vm/native.c中,如下:

代码语言:javascript复制
bool dvmLoadNativeCode(const char* pathName, Object* classLoader)
{
    SharedLib* pEntry;
    void* handle;

    LOGD("Trying to load lib %s %pn", pathName, classLoader);

    /*
     * See if we've already loaded it.  If we have, and the class loader
     * matches, return successfully without doing anything.
     */
    pEntry = findSharedLibEntry(pathName);
    if (pEntry != NULL) {
        if (pEntry->classLoader != classLoader) {
            LOGW("Shared lib '%s' already opened by CL %p; can't open in %pn",
                pathName, pEntry->classLoader, classLoader);
            return false;
        }
        LOGD("Shared lib '%s' already loaded in same CL %pn",
            pathName, classLoader);
        if (!checkOnLoadResult(pEntry))
            return false;
        return true;
    }

    Thread* self = dvmThreadSelf();
    int oldStatus = dvmChangeStatus(self, THREAD_VMWAIT);
    handle = dlopen(pathName, RTLD_LAZY);
    dvmChangeStatus(self, oldStatus);

    if (handle == NULL) {
        LOGI("Unable to dlopen(%s): %sn", pathName, dlerror());
        return false;
    }

    /* create a new entry */
    SharedLib* pNewEntry;
    pNewEntry = (SharedLib*) calloc(1, sizeof(SharedLib));
    pNewEntry->pathName = strdup(pathName);
    pNewEntry->handle = handle;
    pNewEntry->classLoader = classLoader;
    dvmInitMutex(&pNewEntry->onLoadLock);
    pthread_cond_init(&pNewEntry->onLoadCond, NULL);
    pNewEntry->onLoadThreadId = self->threadId;

    /* try to add it to the list */
    SharedLib* pActualEntry = addSharedLibEntry(pNewEntry);

    if (pNewEntry != pActualEntry) {
        LOGI("WOW: we lost a race to add a shared lib (%s CL=%p)n",
            pathName, classLoader);
        freeSharedLibEntry(pNewEntry);
        return checkOnLoadResult(pActualEntry);
    } else {
        LOGD("Added shared lib %s %pn", pathName, classLoader);

        bool result = true;
        void* vonLoad;
        int version;

        vonLoad = dlsym(handle, "JNI_OnLoad");
        if (vonLoad == NULL) {
            LOGD("No JNI_OnLoad found in %s %pn", pathName, classLoader);
        } 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.)
             */
            OnLoadFunc func = vonLoad;
            Object* prevOverride = self->classLoaderOverride;

            self->classLoaderOverride = classLoader;
            oldStatus = dvmChangeStatus(self, THREAD_NATIVE);
            LOGV("    calling JNI_OnLoad(%s)n", pathName);
            version = (*func)(gDvm.vmList, NULL);
            dvmChangeStatus(self, oldStatus);
            self->classLoaderOverride = prevOverride;

            if (version != JNI_VERSION_1_2 && version != JNI_VERSION_1_4 &&
                version != JNI_VERSION_1_6)
            {
                LOGW("JNI_OnLoad returned bad version (%d) in %s %pn",
                    version, pathName, classLoader);
                /*
                 * It's unwise to call dlclose() here, but we can mark it
                 * as bad and ensure that future load attempts will fail.
                 *
                 * We don't know how far JNI_OnLoad got, so there could
                 * be some partially-initialized stuff accessible through
                 * newly-registered native method calls.  We could try to
                 * unregister them, but that doesn't seem worthwhile.
                 */
                result = false;
            } else {
                LOGV("    finished JNI_OnLoad %sn", pathName);
            }
        }

        if (result)
            pNewEntry->onLoadResult = kOnLoadOkay;
        else
            pNewEntry->onLoadResult = kOnLoadFailed;

        pNewEntry->onLoadThreadId = 0;

        /*
         * Broadcast a wakeup to anybody sleeping on the condition variable.         */
        dvmLockMutex(&pNewEntry->onLoadLock);
        pthread_cond_broadcast(&pNewEntry->onLoadCond);
        dvmUnlockMutex(&pNewEntry->onLoadLock);
        return result;
    }
}

dvmLoadNativeCode()首先会检测是否已经加载过这个so(findSharedLibEntry),如果已经加载过了,那么直接返回即可;如果没有加载,那么重新加载一遍,加载的过程可以用下面的流程来描述:

  1. 调用dlopen() 打开一个so文件,取得该so的文件句柄;
  2. 调用dlsym()函数,查找到so文件中的JNI_OnLoad()这个函数的函数指针;
  3. 执行JNI_OnLoad()函数;

0 人点赞