一文了解 Java 中 so 文件的加载原理

news2024/9/24 21:21:38

在这里插入图片描述

前言

无论是 Android 开发者还是 Java 工程师应该都有使用过 JNI 开发,但对于 JVM 如何加载 so、Android 系统如何加载 so,可能鲜有时间了解。

本文通过代码、流程解释,带大家快速了解其加载原理,扫清困惑。

1. System#load() + loadLibrary()

1.1 load()

System 提供的 load() 用于指定 so 的完整的路径名且带文件后缀并加载,等同于调用 Runtime 类提供的 load()。

If the filename argument, when stripped of any platform-specific library prefix, path, and file extension, indicates a library whose name is, for example, L, and a native library called L is statically linked with the VM, then the JNI_OnLoad_L function exported by the library is invoked rather than attempting to load a dynamic library.

Eg.

System.load("/sdcard/path/libA.so")

步骤简述:

  1. 通过 Reflection 获取调用来源的 Class 实例

  2. 接着调用 Runtime 的 load0() 实现

    • load0() 首先获取系统的 SecurityManager

    • 当 SecurityManager 存在的话检查目标 so 文件的访问权限:权限不足的话打印拒绝信息、抛出 SecurityException ,如果 name 参数为空,抛出 NullPointerException

    • 如果 so 文件名非绝对路径的话,并不支持,并抛出 UnsatisfiedLinkError,message 为:

      Expecting an absolute path of the library: xxx

    • 针对 so 文件的权限检查和名称检查均通过的话,继续调用 ClassLoader 的 loadLibrary() 实现,需要留意的是绝对路径参数为 true

// java/lang/System.java
    public static void load(String filename) {
        Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
    }

// java/lang/Runtime.java
    synchronized void load0(Class<?> fromClass, String filename) {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkLink(filename);
        }
        if (!(new File(filename).isAbsolute())) {
            throw new UnsatisfiedLinkError(
                "Expecting an absolute path of the library: " + filename);
        }
        ClassLoader.loadLibrary(fromClass, filename, true);
    }

1.2 loadLibrary()

System 类提供的 loadLibrary() 用于指定 so 的名称并加载,等同于调用 Runtime 类提供的 loadLibrary()。在 Android 平台系统会自动去系统目录(/system/lib64/)、应用 lib 目录(/data/app/xxx/lib64/)下去找 libname 参数拼接了 lib 前缀的库文件。

The libname argument must not contain any platform specific prefix, file extension or path.

If a native library called libname is statically linked with the VM, then the JNI_OnLoad_libname function exported by the library is invoked.

Eg.

System.loadLibrary("A")

步骤简述:

  1. 同样通过 Reflection 获取调用来源的 Class 实例

  2. 接着调用 Runtime 的 loadLibrary0() 实现

    • loadLibrary0() 首先获取系统的 SecurityManager,并检查目标 so 文件的访问权限:权限不足或文件名为空的话和上面一样抛出 Exception

    • 确保 so 名称不包含 /,反之,抛出 UnsatisfiedLinkError,message 为:

      Directory separator should not appear in library name: xxx

    • 检查通过后,同样调用 ClassLoader 的 loadLibrary() 实现继续下一步,只不过绝对路径参数为 false

// java/lang/System.java
    public static void loadLibrary(String libname) {
        Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
    }

// java/lang/Runtime.java
    synchronized void loadLibrary0(Class<?> fromClass, String libname) {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkLink(libname);
        }
        if (libname.indexOf((int)File.separatorChar) != -1) {
            throw new UnsatisfiedLinkError(
    "Directory separator should not appear in library name: " + libname);
        }
        ClassLoader.loadLibrary(fromClass, libname, false);
    }

2. ClassLoader#loadLibrary()

上面的调用栈可以看到无论是 load() 还是 loadLibrary() 最终都是调用 ClassLoaderloadLibrary(),主要区别在于 name 参数是 lib 完整路径、还是 lib 名称,以及是否是绝对路径参数。

  1. 首先通过 getClassLoader() 获得加载源所属的 ClassLoader 实例

  2. 确保存放 libraries 路径的字符串数组 sys_paths 不为空

    • 尚且为空的话,调用 initializePath(“java.library.path”) 先初始化 usr 路径字符串数组,再调用 initializePath(“sun.boot.library.path”) 初始化 system 路径字符串数组。initializePath() 具体见下章节
  3. 依据是否 isAbsolute 决定是否直接加载 library

    • name 是绝对路径的话,直接创建 File 实例,调用 loadLibrary0(),继续加载该文件。具体见下章节

      • 检查 loadLibrary0 的结果:true:即表示加载成功,结束;false:即表示加载失败,抛出 UnsatisfiedLinkError

        Can’t load xxx

    • name 非绝对路径并且获取的 ClassLoader 存在的话,通过 findLibrary() ,根据 so 名称获得 lib 绝对路径,并创建指向该路径的 File 实例 libfile

      • 并确保该文件的路径是绝对路径。反之,抛出 UnsatisfiedLinkError

        ClassLoader.findLibrary failed to return an absolute path: xxx

      • 此后也是调用 loadLibrary0() 继续加载该文件,并检查 loadLibrary0 的结果,处理同上

  4. 假使 ClassLoader 不存在:遍历 system 路径字符串数组的元素,

    • 通过 mapLibraryName() 分别将 lib name 映射到平台关联的 lib 完整名称并返回,具体见下章节

    • 创建当前遍历的 path 下 libfile 实例

    • 调用 loadLibrary0() 继续加载该文件,并检查结果:

      • true 则直接结束

      • false 的话,通过 mapAlternativeName() 获取该 lib 可能存在的替代文件名,比如将后缀替换为 jnilib

        • 如果再度 map 后的 libfile 不为空,调用 loadLibrary0() 再度加载该文件并检查结果,true 则直接结束;反之,进入下一次循环
  5. 至此,如果仍未成功找到 library 文件,则在 ClassLoader 存在的情况下,到 usr 路径字符串数组中查找

    • 遍历 usr 路径字符串数组的元素
      • 后续逻辑和上述一致,只是 map 时候的前缀不同,是 usr_paths 的元素
  6. 最终进行默认处理,即抛出 UnsatisfiedLinkError,提示在 java.library.path propery 代表的路径下也未找到 so 文件

no xx in java.library.path

// java/lang/ClassLoader.java
    static void loadLibrary(Class<?> fromClass, String name,
                            boolean isAbsolute) {
        ClassLoader loader =
            (fromClass == null) ? null : fromClass.getClassLoader();
        if (sys_paths == null) {
            usr_paths = initializePath("java.library.path");
            sys_paths = initializePath("sun.boot.library.path");
        }
        if (isAbsolute) {
            if (loadLibrary0(fromClass, new File(name))) {
                return;
            }
            throw new UnsatisfiedLinkError("Can't load library: " + name);
        }
        if (loader != null) {
            String libfilename = loader.findLibrary(name);
            if (libfilename != null) {
                File libfile = new File(libfilename);
                if (!libfile.isAbsolute()) {
                    throw new UnsatisfiedLinkError(...);
                }
                if (loadLibrary0(fromClass, libfile)) {
                    return;
                }
                throw new UnsatisfiedLinkError("Can't load " + libfilename);
            }
        }
        for (int i = 0 ; i < sys_paths.length ; i++) {
            File libfile = new File(sys_paths[i], System.mapLibraryName(name));
            if (loadLibrary0(fromClass, libfile)) {
                return;
            }
            libfile = ClassLoaderHelper.mapAlternativeName(libfile);
            if (libfile != null && loadLibrary0(fromClass, libfile)) {
                return;
            }
        }
        if (loader != null) {
            for (int i = 0 ; i < usr_paths.length ; i++) {
                File libfile = new File(usr_paths[i],
                                        System.mapLibraryName(name));
                if (loadLibrary0(fromClass, libfile)) {
                    return;
                }
                libfile = ClassLoaderHelper.mapAlternativeName(libfile);
                if (libfile != null && loadLibrary0(fromClass, libfile)) {
                    return;
                }
            }
        }
        // Oops, it failed
        throw new UnsatisfiedLinkError("no " + name + " in java.library.path");
    }

3. ClassLoader#initializePath()

System 中获取对应 property 代表的 path 到数组中。

  1. 先调用 getProperty() 从 JVM 中取出配置的路径,默认的是 “”

    • 其中的 checkKey() 将检查 key 名称是否合法,null 的话抛出 NullPointerException

      key can’t be null

      如果为"",抛出 IllegalArgumentException

      key can’t be empty

    • 后面通过 getSecurityManager() 获取 SecurityManager 实例,检查是否存在该 property 的访问权限

  2. 如果允许引用路径元素并且 \ 存在的话,将路径字符串的 char 取出进行拼接、计算得到路径字符串数组

  3. 反之通过 indexOf(/) 统计 / 出现的次数,并创建一个 / 次数 + 1 的数组

  4. 遍历该路径字符串,通过 substring() 将各 / 的中间 path 内容提取到上述数组中

  5. 最后返回得到的 path 数组

// java/lang/ClassLoader.java
    private static String[] initializePath(String propname) {
        String ldpath = System.getProperty(propname, "");
        String ps = File.pathSeparator;
        ...

        i = ldpath.indexOf(ps);
        n = 0;
        while (i >= 0) {
            n++;
            i = ldpath.indexOf(ps, i + 1);
        }

        String[] paths = new String[n + 1];
        n = i = 0;
        j = ldpath.indexOf(ps);
        while (j >= 0) {
            if (j - i > 0) {
                paths[n++] = ldpath.substring(i, j);
            } else if (j - i == 0) {
                paths[n++] = ".";
            }
            i = j + 1;
            j = ldpath.indexOf(ps, i);
        }
        paths[n] = ldpath.substring(i, ldlen);
        return paths;
    }

4. ClassLoader#findLibrary()

findLibrary() 将到 ClassLoader 中查找 lib,取决于各 JVM 的具体实现。比如可以看看 Android 上的实现。

  1. DexPathList 的具体实现中调用
  2. 首先通过 System 类的 mapLibraryName() 中获得 mapping 后的 lib 全名,细节见下章节
  3. 遍历存放 native lib 路径元素数组 nativeLibraryPathElements
  4. 逐个调用各元素的 findNativeLibrary() 实现去寻找
  5. 一经找到立即返回,遍历结束仍未发现的话返回 null
// android/libcore/dalvik/src/main/java/dalvik/system/
// BaseDexClassLoader.java
   public String findLibrary(String name) {
        return pathList.findLibrary(name);
    }

// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.java
    public String findLibrary(String libraryName) {
        // 到 System 中获得 mapping 后的 lib 全名
        String fileName = System.mapLibraryName(libraryName);

        // 到存放 native lib 路径数组中遍历
        for (NativeLibraryElement element : nativeLibraryPathElements) {
            String path = element.findNativeLibrary(fileName);

            // 一旦找到立即返回并结束,反之进入下一次循环
            if (path != null) {
                return path;
            }
        }

        // 路径中全找遍了,仍未找到则返回 null
        return null;
    }

4.1 System#mapLibraryName()

mapLibraryName() 的作用很简单,即将 lib 名称 mapping 到完整格式的名称,比如输入 opencv 得到的是 libopencv.so。如果遇到名称为空或者长度超上限 240 的话,将抛出相应 Exception。

// java/lang/System.java
public static native String mapLibraryName(String libname);

其是 native 方法,具体实现位于 JDK Native Source Code 中,可在如下网站中看到:

  • http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/native
// native/java/lang/System.c

#define JNI_LIB_PREFIX "lib"
#define JNI_LIB_SUFFIX ".so"

Java_java_lang_System_mapLibraryName(JNIEnv *env, jclass ign, jstring libname)
{
    // 定义最后名称的 Sring 长度变量
    int len;
    // 并获取 lib 前缀、后缀的字符串常量的长度
    int prefix_len = (int) strlen(JNI_LIB_PREFIX);
    int suffix_len = (int) strlen(JNI_LIB_SUFFIX);

    // 定义临时的存放最后名称的 char 数组
    jchar chars[256];
    // 如果 libname 参数为空,抛出 NPE
    if (libname == NULL) {
        JNU_ThrowNullPointerException(env, 0);
        return NULL;
    }
    // 获取 libname 长度
    len = (*env)->GetStringLength(env, libname);
    // 如果大于 240 的话抛出 IllegalArgumentException
    if (len > 240) {
        JNU_ThrowIllegalArgumentException(env, "name too long");
        return NULL;
    }
    
    // 将前缀 ”lib“ 的字符拷贝到临时的 char 数组头部
    cpchars(chars, JNI_LIB_PREFIX, prefix_len);
    // 将 lib 名称从字符串里拷贝到 char 数组的 “lib” 后面
    (*env)->GetStringRegion(env, libname, 0, len, chars + prefix_len);
    // 更新名称长度为:前缀+ lib 名称
    len += prefix_len;
    // 将后缀 ”.so“ 的字符拷贝到临时的 char 数组里的 lib 名称后
    cpchars(chars + len, JNI_LIB_SUFFIX, suffix_len);
    // 再次更新名称长度为:前缀+ lib 名称 + 后缀
    len += suffix_len;

    // 从 char 数组里提取当前长度的复数 char 成员到新创建的 String 对象中返回
    return (*env)->NewString(env, chars, len);
}

static void cpchars(jchar *dst, char *src, int n)
{
    int i;
    for (i = 0; i < n; i++) {
        dst[i] = src[i];
    }
}

逻辑很清晰,检查 lib 名称参数是否合法,之后便是将名称分别加上前后缀到临时字符数组中,最后转为字符串返回。

4.2 nativeLibraryPathElements()

nativeLibraryPathElements 数组来源于获取到的所有 native Library 目录后转换而来。

// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.java
    public DexPathList(ClassLoader definingContext, String librarySearchPath) {
        ...
        this.nativeLibraryPathElements = makePathElements(getAllNativeLibraryDirectories());
    }

所有 native Library 目录除了包含应用自身的 library 目录列表以外,还包括了系统的列表部分。

// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.java
    private List<File> getAllNativeLibraryDirectories() {
        List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
        allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
        return allNativeLibraryDirectories;
    }

    /** List of application native library directories. */
    private final List<File> nativeLibraryDirectories;

    /** List of system native library directories. */
    private final List<File> systemNativeLibraryDirectories;

应用自身的 library 目录列表来自于 DexPathList 初始化时传入的 librarySearchPath 参数,splitPaths() 负责去该 path 下遍历各级目录得到对应数组。

// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.java
    public DexPathList(ClassLoader definingContext, String librarySearchPath) {
        ...
        this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
    }

    private static List<File> splitPaths(String searchPath, boolean directoriesOnly) {
        List<File> result = new ArrayList<>();

        if (searchPath != null) {
            for (String path : searchPath.split(File.pathSeparator)) {
                if (directoriesOnly) {
                    try {
                        StructStat sb = Libcore.os.stat(path);
                        if (!S_ISDIR(sb.st_mode)) {
                            continue;
                        }
                    } catch (ErrnoException ignored) {
                        continue;
                    }
                }
                result.add(new File(path));
            }
        }

        return result;
    }

系统列表则来自于系统的 path 路径,调用 splitPaths() 的第二个参数不同,促使其在分割的时候只处理目录类型的部分,纯文件的话跳过。

// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.java
    public DexPathList(ClassLoader definingContext, String librarySearchPath) {
        ...
        this.systemNativeLibraryDirectories =
                splitPaths(System.getProperty("java.library.path"), true);
        ...
    }

拿到 path 文件列表之后就是调用 makePathElements 转成对应元素数组。

  1. 按照列表长度创建等长的 Element 数组
  2. 遍历 path 列表
  3. 如果 path 包含 “!/” 的话,将其拆分为 path 和 zipDir 两部分,并创建 NativeLibraryElement 实例
  4. 反之,如果是目录的话,直接用 path 创建 NativeLibraryElement 实例,zipDir 参数则为空
// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.java
    private static NativeLibraryElement[] makePathElements(List<File> files) {
        NativeLibraryElement[] elements = new NativeLibraryElement[files.size()];
        int elementsPos = 0;
        for (File file : files) {
            String path = file.getPath();

            if (path.contains(zipSeparator)) {
                String split[] = path.split(zipSeparator, 2);
                File zip = new File(split[0]);
                String dir = split[1];
                elements[elementsPos++] = new NativeLibraryElement(zip, dir);
            } else if (file.isDirectory()) {
                // We support directories for looking up native libraries.
                elements[elementsPos++] = new NativeLibraryElement(file);
            }
        }
        if (elementsPos != elements.length) {
            elements = Arrays.copyOf(elements, elementsPos);
        }
        return elements;
    }

4.3 findNativeLibrary()

findNativeLibrary() 将先确保当 zip 目录存在的情况下内部处理 zip 的 ClassPathURLStreamHandler 实例执行了创建。

  • 如果 zip 目录不存在(一般情况下都是不存在的)直接判断该路径下 lib 文件是否可读,YES 则返回 path/name、反之返回 null
  • zip 目录存在并且 ClassPathURLStreamHandler 实例也创建完毕的话,检查该 name 的 zip 文件的存在。并在 YES 的情况下,在 path 和 name 之间跟上 zip 目录并返回,即:path/!/zipDir/name
// DexPathList.java
// android/.../libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
    private static final String zipSeparator = "!/";

    static class NativeLibraryElement {
        public String findNativeLibrary(String name) {
            // 确保 element 初始化完成
            maybeInit();

            if (zipDir == null) {
                // 如果 zip 目录为空,则直接创建该 path 下该文件的 File 实例
                // 可读的话则返回
                String entryPath = new File(path, name).getPath();
                if (IoUtils.canOpenReadOnly(entryPath)) {
                    return entryPath;
                }
            } else if (urlHandler != null) {
                // zip 目录并且 urlHandler 都存在
                // 创建该 zip 目录下 lib 文件的完整名称
                String entryName = zipDir + '/' + name;
                // 如果该名称的压缩包是否存在的话
                if (urlHandler.isEntryStored(entryName)) {
                    // 返回:路径/zip目录/lib 名称的结果出去
                    return path.getPath() + zipSeparator + entryName;
                }
            }

            return null;
        }

        // 主要是确保在 zipDir 不为空的情况下
        // 内部处理 zip 的 urlHandler 实例已经创建完毕
        public synchronized void maybeInit() {
            ...
        }
    }

5. ClassLoader#loadLibrary0()

  1. 调用静态内部类 NativeLibrary 的 native 方法 findBuiltinLib() 检查是否是内置的动态链接库,细节见如下章节

    • 如果不是内置的 library,通过 AccessController 检查该 library 文件是否存在
      • 不存在则加载失败并结束
      • 存在则到本 ClassLoader 已加载 library 的 nativeLibraries Vector 或系统 class 的已加载 library Vector systemNativeLibraries 中查找是否加载过
        • 已加载过则结束
        • 反之,继续加载的任务
  2. 到所有 ClassLoader 已加载过的 library Vector loadedLibraryNames 里再次检查是否加载过,如果不存在的话,抛出 UnsatisfiedLinkError:

    Native Library xxx already loaded in another classloader

  3. 到正在加载/卸载 library 的 nativeLibraryContext Stack 中检查是否已经处理中了

    • 存在并且 ClassLoader 来源匹配,则结束加载

    • 存在但 ClassLoader 来源不同,则抛出 UnsatisfiedLinkError:

      Native Library xxx is being loaded in another classloader

    • 反之,继续加载的任务

  4. 依据 ClassLoader、library 名称、是否内置等信息,创建 NativeLibrary 实例并入 nativeLibraryContext 栈

  5. 此后,交由 NativeLibrary load,细节亦见如下章节,并在 load 后出栈

  6. 最后根据 load 的结果决定是否将加载记录到对应的 Vector 当中

// java/lang/ClassLoader.java
    private static boolean loadLibrary0(Class<?> fromClass, final File file) {
        // 获取是否是内置动态链接库
        String name = NativeLibrary.findBuiltinLib(file.getName());
        boolean isBuiltin = (name != null);
        if (!isBuiltin) {
            // 不是内置的话,检查文件是否存在
            boolean exists = AccessController.doPrivileged(
                new PrivilegedAction<Object>() {
                    public Object run() {
                        return file.exists() ? Boolean.TRUE : null;
                    }})
                != null;
            if (!exists) {
                return false;
            }
            try {
                name = file.getCanonicalPath();
            } catch (IOException e) {
                return false;
            }
        }
        ClassLoader loader =
            (fromClass == null) ? null : fromClass.getClassLoader();
        Vector<NativeLibrary> libs =
            loader != null ? loader.nativeLibraries : systemNativeLibraries;
        synchronized (libs) {
            int size = libs.size();
            // 检查是否已经加载过
            for (int i = 0; i < size; i++) {
                NativeLibrary lib = libs.elementAt(i);
                if (name.equals(lib.name)) {
                    return true;
                }
            }

            synchronized (loadedLibraryNames) {
                // 再次检查所有 library 加载历史中是否存在
                if (loadedLibraryNames.contains(name)) {
                    throw new UnsatisfiedLinkError(...);
                }
                int n = nativeLibraryContext.size();
                // 检查是否已经在加载中了
                for (int i = 0; i < n; i++) {
                    NativeLibrary lib = nativeLibraryContext.elementAt(i);
                    if (name.equals(lib.name)) {
                        if (loader == lib.fromClass.getClassLoader()) {
                            return true;
                        } else {
                            throw new UnsatisfiedLinkError(...);
                        }
                    }
                }
                // 创建 NativeLibrary 实例继续加载
                NativeLibrary lib = new NativeLibrary(fromClass, name, isBuiltin);
                // 并在加载前后压栈和出栈
                nativeLibraryContext.push(lib);
                try {
                    lib.load(name, isBuiltin);
                } finally {
                    nativeLibraryContext.pop();
                }
                // 加载成功的将该 library 名称缓存到 vector 中
                if (lib.loaded) {
                    loadedLibraryNames.addElement(name);
                    libs.addElement(lib);
                    return true;
                }
                return false;
            }
        }
    }

5.1 findBuiltinLib()

  1. 首先一如既往地先检查 library name 是否为空,为空则抛出 Error

    NULL filename for native library

  2. 将 string 类型的名称转为 char 指针,失败的话抛出 OutOfMemoryError

  3. 检查名称长度是否短于最起码的 lib.so 几位,失败的话返回 NULL 结束

  4. 创建 library 名称指针 libName 并分配内存

  5. 从 char 指针提取 libxxx.so 中 xxx.so 部分到 libName 中

  6. 将 libName 中 .so 的 . 位置替换成 \0

  7. 调用 findJniFunction() 依据 handle 指针,library 名称检查该 library 的 JNI_OnLoad() 是否存在

    • 存在则释放 libName 内存并返回该函数地址
    • 反之,释放内存并返回 NULL 结束
// native/java/lang/ClassLoader.c
Java_java_lang_ClassLoader_00024NativeLibrary_findBuiltinLib
  (JNIEnv *env, jclass cls, jstring name)
{
    const char *cname;
    char *libName;
    ...
    // 检查名称是否为空
    if (name == NULL) {
        JNU_ThrowInternalError(env, "NULL filename for native library");
        return NULL;
    }

    procHandle = getProcessHandle();
    cname = JNU_GetStringPlatformChars(env, name, 0);
    // 检查 char 名称指针是否为空
    if (cname == NULL) {
        JNU_ThrowOutOfMemoryError(env, NULL);
        return NULL;
    }

    // 检查名称长度
    len = strlen(cname);
    if (len <= (prefixLen+suffixLen)) {
        JNU_ReleaseStringPlatformChars(env, name, cname);
        return NULL;
    }
    // 提取 library 名称(取出前后缀)
    libName = malloc(len + 1); //+1 for null if prefix+suffix == 0
    if (libName == NULL) {
        JNU_ReleaseStringPlatformChars(env, name, cname);
        JNU_ThrowOutOfMemoryError(env, NULL);
        return NULL;
    }
    if (len > prefixLen) {
        strcpy(libName, cname+prefixLen);
    }
    JNU_ReleaseStringPlatformChars(env, name, cname);
    libName[strlen(libName)-suffixLen] = '\0';

    // 检查 JNI_OnLoad() 释放存在
    ret = findJniFunction(env, procHandle, libName, JNI_TRUE);
    if (ret != NULL) {
        lib = JNU_NewStringPlatform(env, libName);
        free(libName);
        return lib;
    }
    free(libName);
    return NULL;
}

5.2 findJniFunction()

findJniFunction() 用于到 library 指针、已加载/卸载的 JNI 数组中查找该 library 名称所对应的 JNI_ONLOAD、JNI_ONUNLOAD 的函数地址。

// native/java/lang/ClassLoader.c
static void *findJniFunction(JNIEnv *env, void *handle,
                                    const char *cname, jboolean isLoad) {
    const char *onLoadSymbols[] = JNI_ONLOAD_SYMBOLS;
    const char *onUnloadSymbols[] = JNI_ONUNLOAD_SYMBOLS;
    void *entryName = NULL;
    ...
    // 如果是加载,则到 JNI_ONLOAD_SYMBOLS 中获取函数数组和长度
    if (isLoad) {
        syms = onLoadSymbols;
        symsLen = sizeof(onLoadSymbols) / sizeof(char *);
    } else {
        // 反之,则到 JNI_ONUNLOAD_SYMBOLS 中获取卸载函数数组和长度
        syms = onUnloadSymbols;
        symsLen = sizeof(onUnloadSymbols) / sizeof(char *);
    }
    // 遍历该数组,调用 JVM_FindLibraryEntry()
    // 逐个查找 JNI_On(Un)Load<_libname> function 是否存在
    for (i = 0; i < symsLen; i++) {
        // cname + sym + '_' + '\0'
        if ((len = (cname != NULL ? strlen(cname) : 0) + strlen(syms[i]) + 2) >
            FILENAME_MAX) {
            goto done;
        }
        jniFunctionName = malloc(len);
        if (jniFunctionName == NULL) {
            JNU_ThrowOutOfMemoryError(env, NULL);
            goto done;
        }
        buildJniFunctionName(syms[i], cname, jniFunctionName);
        entryName = JVM_FindLibraryEntry(handle, jniFunctionName);
        free(jniFunctionName);
        if(entryName) {
            break;
        }
    }

 done:
    // 如果没有找到,默认返回 NULL
    return entryName;
}

5.3 JVM_FindLibraryEntry()

JVM_FindLibraryEntry() 调用的是平台相关的 dll_lookup(),依据 library 指针和 function 名称。

// vm/prims/jvm.cpp
JVM_LEAF(void*, JVM_FindLibraryEntry(void* handle, const char* name))
  JVMWrapper2("JVM_FindLibraryEntry (%s)", name);
  return os::dll_lookup(handle, name);
JVM_END

6. NativeLibrary#load()

NativeLibrary 是定义在 ClassLoader 内的静态内部类,其代表着已加载 library 的实例,包含了该 library 的指针、所需的 JNI 版本、加载的 Class 来源、名称、是否是内置 library、是否加载过重要信息。

以及核心的加载 load、卸载 unload native 实现。

// java/lang/ClassLoader.java
    static class NativeLibrary {
        long handle;
        private int jniVersion;
        private final Class<?> fromClass;
        String name;
        boolean isBuiltin;
        boolean loaded;

        native void load(String name, boolean isBuiltin);
        native void unload(String name, boolean isBuiltin);
        static native String findBuiltinLib(String name);
        ...
    }

本章节我们着重看下 load() 的关键实现:

  1. 首先调用 initIDs() 初始化 ID 等基本数据

    • 如果 ClassLoader$NativeLibrary 内部类、handle 等属性有一不存在的话,返回 FALSE 并结束加载
    • 通过检查的话初始化 procHandle 指针
  2. 其次通过 JNU_GetStringPlatformChars() 将 String 类型的 library 名称转为 char 类型,如果名称为空的话结束加载

  3. 如果不是内置的 so,需要调用 JVM_LoadLibrary() 加载得到指针(见下章节),反之沿用上述的 procHandle 指针即可

  4. 如果 so 指针存在的话,通过 findJniFunction() 和指针参数获取 JNI_OnLoad() 的地址

    • 如果 JNI_OnLoad() 获取成功,则调用它并得到该 so 要求的 jniVersion

    • 反之设置为默认值 0x00010001,即 JNI_VERSION_1_1,1.1

    • 接着调用 JVM_IsSupportedJNIVersion() 检查 JVM 是否支持该版本,调用的是 Threads 的 is_supported_jni_version_including_1_1()

      • 如果不支持或者是内置 so 同时版本低于 1.8,抛出 UnsatisfiedLinkError:

        unsupported JNI version xxx required by yyy

      • 反之表示加载成功

  5. 反之,抛出异常 ExceptionOccurred

// native/java/lang/ClassLoader.c
Java_java_lang_ClassLoader_00024NativeLibrary_load
  (JNIEnv *env, jobject this, jstring name, jboolean isBuiltin)
{
    const char *cname;
    ...
    void * handle;

    if (!initIDs(env)) return;

    cname = JNU_GetStringPlatformChars(env, name, 0);
    if (cname == 0) return;

    handle = isBuiltin ? procHandle : JVM_LoadLibrary(cname);
    if (handle) {
        JNI_OnLoad_t JNI_OnLoad;
        JNI_OnLoad = (JNI_OnLoad_t)findJniFunction(env, handle,
                                               isBuiltin ? cname : NULL,
                                               JNI_TRUE);
        if (JNI_OnLoad) {
            ...
            jniVersion = (*JNI_OnLoad)(jvm, NULL);
        } else {
            jniVersion = 0x00010001;
        }
        ...
        if (!JVM_IsSupportedJNIVersion(jniVersion) ||
            (isBuiltin && jniVersion < JNI_VERSION_1_8)) {
            char msg[256];
            jio_snprintf(msg, sizeof(msg),
                         "unsupported JNI version 0x%08X required by %s",
                         jniVersion, cname);
            JNU_ThrowByName(env, "java/lang/UnsatisfiedLinkError", msg);
            if (!isBuiltin) {
                JVM_UnloadLibrary(handle);
            }
            goto done;
        }
        (*env)->SetIntField(env, this, jniVersionID, jniVersion);
    } else {
        cause = (*env)->ExceptionOccurred(env);
        if (cause) {
            (*env)->ExceptionClear(env);
            (*env)->SetLongField(env, this, handleID, (jlong)0);
            (*env)->Throw(env, cause);
        }
        goto done;
    }
    (*env)->SetLongField(env, this, handleID, ptr_to_jlong(handle));
    (*env)->SetBooleanField(env, this, loadedID, JNI_TRUE);

 done:
    JNU_ReleaseStringPlatformChars(env, name, cname);
}

static jboolean initIDs(JNIEnv *env)
{
    if (handleID == 0) {
        jclass this =
            (*env)->FindClass(env, "java/lang/ClassLoader$NativeLibrary");
        if (this == 0)
            return JNI_FALSE;
        handleID = (*env)->GetFieldID(env, this, "handle", "J");
        if (handleID == 0)
            return JNI_FALSE;
        ...
        procHandle = getProcessHandle();
    }
    return JNI_TRUE;
}

7. JVM_LoadLibrary()

JVM_LoadLibrary() 是 JVM 这层加载 library 的最后一个实现,具体步骤如下:

  1. 定义 1024 长度的 char 数组和接收加载结果的指针
  2. 调用 dll_load() 加载 library,其细节见下章节
  3. 加载失败的话,打印 library 名称和错误 message
  4. 同时抛出 UnsatisfiedLinkError
  5. 反之将加载结果返回
// vm/prims/jvm.cpp
JVM_ENTRY_NO_ENV(void*, JVM_LoadLibrary(const char* name))
  JVMWrapper2("JVM_LoadLibrary (%s)", name);
  char ebuf[1024];
  void *load_result;
  {
    ThreadToNativeFromVM ttnfvm(thread);
    load_result = os::dll_load(name, ebuf, sizeof ebuf);
  }
  if (load_result == NULL) {
    char msg[1024];
    jio_snprintf(msg, sizeof msg, "%s: %s", name, ebuf);
    Handle h_exception =
      Exceptions::new_exception(...);
    THROW_HANDLE_0(h_exception);
  }
  return load_result;
JVM_END

8. dll_load()

dll_load() 的实现跟平台相关,比如 bsd 平台就是调用标准库的 dlopen(),而其最终的结果来自于 do_dlopen(),其将通过 find_library() 得到 soinfo 实例,内部将执行 to_handle() 得到 library 的指针。

// bionic/libdl/libdl.cpp
void* dlopen(const char* filename, int flag) {
  const void* caller_addr = __builtin_return_address(0);
  return __loader_dlopen(filename, flag, caller_addr);
}

void* __loader_dlopen(const char* filename, int flags, const void* caller_addr) {
  return dlopen_ext(filename, flags, nullptr, caller_addr);
}

static void* dlopen_ext(...) {
  ScopedPthreadMutexLocker locker(&g_dl_mutex);
  g_linker_logger.ResetState();
  void* result = do_dlopen(filename, flags, extinfo, caller_addr);
  if (result == nullptr) {
    __bionic_format_dlerror("dlopen failed", linker_get_error_buffer());
    return nullptr;
  }
  return result;
}

void* do_dlopen(...) {
  ...
  if (si != nullptr) {
    void* handle = si->to_handle();
    si->call_constructors();
    failure_guard.Disable();
    return handle;
  }

  return nullptr;
}

9. JNI_OnLoad()

JNI_OnLoad() 定义在 jni.h 中,当 library 被 JVM 加载时会回调,该方法内一般会通过 registerNatives() 注册 native 方法并返回该 library 所需的 JNI 版本。

该头文件还定义了其他函数和常量,比如 JNI 1.1 等数值。

// jni.h
...
struct JNIEnv_ {
    ...
    jint RegisterNatives(jclass clazz, const JNINativeMethod *methods,
                         jint nMethods) {
        return functions->RegisterNatives(this,clazz,methods,nMethods);
    }
    jint UnregisterNatives(jclass clazz) {
        return functions->UnregisterNatives(this,clazz);
    }
}
...
/* Defined by native libraries. */
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved);

#define JNI_VERSION_1_1 0x00010001
...

结语

在这里插入图片描述

总体流程可以归纳如下:

  1. System 类提供的 load() 加载 so 的完整的路径名且带文件后缀,等同于直接调用 Runtime 类提供的 load();loadLibrary() 用于加载指定 so 的名称,等同于调用 Runtime 类提供的 loadLibrary()。
    • 两者都将通过 SecurityManager 检查 so 的访问权限以及名称是否合法
  2. 之后调用 ClassLoader 类的 loadLibrary() 实现,区别在于前者指定的是否是绝对路径的 isAbsolute 参数是否为 true
  3. ClassLoader 首先需要通过 System 提供的 getProperty() 获取 JVM 配置的存放 usrsystem library 路径字符串数组
  4. 如果 library name 非绝对路径,需要先调用 findLibrary() 获取该 name 对应的完整 so 文件,之后再调用 loadLibrary0() 继续
    • ClassLoader 不存在,分别到 system、usr 字符串数组中查找该 so 是否存在
  5. loadLibrary0() 将调用 native 方法 findBuiltinLib() 检查是否是内置的动态链接库,并到加载过 vector、加载中 context 中查找是否已经加载过、加载中
  6. 通过检查的话调用 NativeLibrary 静态内部类继续,事实上是调用 ClassLoader.cload()
  7. 其将调用 jvm.cppJVM_LoadLibrary() 进行 so 的加载获得指针
  8. 根据 OS 的实现,dll_load() 通过 dlopen() 执行 so 的打开和地址返回
  9. 最后通过 findJniFunction() 获取 JNI_OnLoad() 地址进行 native 方法的注册和所需 JNI 版本的收集。

源码地址

  • BaseDexClassLoader.java
  • System.c
  • ClassLoader.c
  • jvm.cpp
  • libdl.cpp
  • jni.h

参考资料

写作本文的时候参考了很多文章的内容,也有部分内容阐述了 JNI 原理以外的东西,大家可以结合着一起看看。

  • Hotspot JNI库文件加载源码解析
  • JNI开发之JNI原理
  • Android JNI:深入分析安卓JNI原理
  • JVM的启动过程
  • 深入理解JNI
  • JNI/NDK入门指南之JavaVM和JNIEnv
  • Android 源码中的 JNI,到底是如何使用的?
  • Java System.load() 与 System.loadLibrary() 区别解析
  • so加载 - Linker跟NameSpace知识
  • Android NDK开发:JNI基础篇
  • Android NDK开发:JNI实战篇
  • JNI 从入门到实践,万字爆肝详解!
  • 如何查看JVM的源码
  • Where to find source code for java.lang native methods?
  • Java native method source code

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/187664.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

3.1.2 访问控制符及修饰符

文章目录1.访问控制符2.静态字段/方法/代码块2.1 静态字段2.2 静态常量2.3 静态方法2.4 特点2.5 static入门案例2.6 静态的调用关系2.7 静态代码块2.7.1 格式2.7.2 特性&#xff1a;2.7.3 执行顺序2.7.4 案例练习3.final的概念3.1 特点3.2 final入门案例1.访问控制符 在JAVA中…

【HBase——陌陌海量存储案例】8. 基于Phoenix消息数据查询(下)

索引示例二&#xff1a;创建本地索引 需求 在程序中&#xff0c;我们可能会根据订单ID、订单状态、支付金额、支付方式、用户ID来查询订单。所以&#xff0c;我们需要在这些列上来查询订单。 针对这种场景&#xff0c;我们可以使用本地索引来提高查询效率。 创建本地索引 cre…

超全小程序开发的学习 知识点

第一章&#xff1a;邂逅小程序开发 01_小程序开发和各个平台小程序的介绍 小程序加载的时候是双线程模型.wxml文件和wxss文件是一个线程&#xff0c;js和json文件是一个线程。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mEP3PUoo-1675132790458…

七步让您的MySQL服务器更安全

本文将以最常见的数据库管理系统——MySQL为例&#xff0c;向您介绍如何通过7步骤来安全加固数据库服务器。 不知您是否发现一种现象&#xff0c;那些初学渗透测试的人员往往过于关注应用的安全性&#xff0c;而对数据库的安全性不太重视。他们殊不知&#xff0c;没有数据库的…

上海亚商投顾:兔年首日开门红 北向资金净流入超186亿

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。市场情绪两市早盘受外围影响大幅高开&#xff0c;随后指数高开低走&#xff0c;板块及个股相对活跃&#xff0c;汽车产业链&a…

视频图像分析处理流程(完整版)

来源&#xff1a;投稿 作者&#xff1a;LSC 编辑&#xff1a;学姐 一、视频分析处理的完整流程 (1)视频编解码的入门知识 尽管压缩工具五花八门&#xff0c;但是他们的目的都只有一个&#xff1a;都是为了减小文件的占用空间。 除去我们常见的.zip&#xff0c;.7z&#xff0…

MyBatis框架如何实现数据查询?有几种方法?

在实际开发中&#xff0c;查询操作通常都会涉及到单条数据的精确查询&#xff0c;以及多条数据的模糊查询。那么使用MyBatis框架是如何进行这两种查询的呢&#xff1f;接下来&#xff0c;本小节将讲解下如何使用MyBatis框架根据客户编号查询客户信息&#xff0c;以及根据客户名…

【前沿技术】在安全且可靠的区块链基础设施中运行业务条线应用

发表时间&#xff1a;2022年4月27日 信息来源&#xff1a;coingeek.com 了解特定企业的业务需求将使你能够构建出一个可扩容的业务条线应用&#xff0c;它将按照你想要的方式进行运作&#xff0c;并在不可篡改的BSV区块链中保存相关记录。 大多数企业都有一个业务条线&#xf…

《吐血整理》保姆级系列教程-玩转Fiddler抓包教程(1)-HTTP和HTTPS基础知识

1.简介 有的小伙伴或者童鞋们可能会好奇地问&#xff0c;不是讲解和分享抓包工具了怎么这里开始讲解HTTP和HTTPS协议了。这是因为你对HTTP协议越了解&#xff0c;你就能越掌握Fiddler的使用方法&#xff0c;反过来你越使用Fiddler&#xff0c;就越能帮助你了解HTTP协议。 Fid…

HashMap原理分析

HashMap原理分析JDK7 HashMap1、模型介绍2、底层实现原理3、描述一下put的过程4、HashMap扩容机制&#xff1a;5、HashMap中的循环链表是如何产生的6、HashMap和HashTable的区别7、HashMap为什么用红黑树而不用B树&#xff1f;JDK8 HashMapJDK7 HashMap 1、模型介绍 HashMap在…

18. time和calendar模块

当代码中需要使用到时间时&#xff0c;我们通常会使用time模块来获取当前时间或者时间戳。 时间戳&#xff1a;从1970年1月1日&#xff08;UTC/GMT的午夜&#xff09;开始所经过的秒数&#xff0c;不考虑闰秒。 1. 时间戳 获取当前时间戳&#xff1a; import timeprint(time…

Springboot+vue预约上门维修服务系统

前端技术&#xff1a;nodejsvueelementui一般是采用前后端分离模式&#xff0c; 后端支持python/php/java/nodejs MTV模式 M:model&#xff0c;模型&#xff0c;负责与数据库交互 V:view&#xff0c;视图是核心&#xff0c;负责接收请求、获取数据、返回结果 T:template&…

并发编程学习(八):ReentrantLock特性、哲学家吃饭问题

ReentrantLock 是java.util.concurrent.locks包下的类。相对于synchronized,它具备如下特性&#xff1a;可中断。可以设置超时时间。可以设置公平锁。支持多个条件变量。即可以有个多个waitset等待队列。与synchronized都支持可重入。ReentrantLock的基本语法&#xff1a;// 获…

c++11 标准模板(STL)(std::multiset)(二)

定义于头文件 <set>template< class Key, class Compare std::less<Key>, class Allocator std::allocator<Key> > class multiset;(1)namespace pmr { template <class Key, class Compare std::less<Key>> using…

Day12【元宇宙的实践构想01】—— 元宇宙概念和发展历程

&#x1f483;&#x1f3fc; 本人简介&#xff1a;男 &#x1f476;&#x1f3fc; 年龄&#xff1a;18 ✍每日一句&#xff1a;【道固远&#xff0c;笃行可至&#xff1b;事虽巨&#xff0c;坚为必成】 &#x1f6a9; 今日留言&#xff1a;亮亮被迫去练科目二啦&#xff0c;定时…

K8S架构熟悉及日常操作

目录 一、架构介绍 二、组件介绍 三、调度介绍 四、CLI指令介绍 五、常见CLI指令 六、常见问题排查思路 一、架构介绍 Kubernetes系统架构为客户端/服务端&#xff08;C/S&#xff09;架构&#xff0c;Master作为服务端&#xff0c;Node作为客户端。 Master服务端也被称…

学习逆向安全的必备基础: 汇编的初步了解

什么是汇编 汇编语言是一种低级编程语言&#xff0c;它使用简单的助记符来表示计算机底层的机器指令。 汇编语言是直接与计算机硬件交互的&#xff0c;它能够控制计算机中的每一个细节。 由于汇编语言非常低级&#xff0c;所以编写汇编程序通常比较困难。不过&#xff0c;汇…

微信怎样开发小程序【公司企业小程序开发】

现在很多公司企业都有自己的小程序&#xff0c;没有小程序的公司企业也会寻找开发小程序的途径。那么今天就给大家简单介绍微信怎样开发小程序&#xff0c;希望对需要开发小程序的公司企业有帮助。 一、注册小程序账号 有一个小程序账号是必须的&#xff0c;小程序账号可以在…

那些外贸老鸟们都在认真使用的8个实用小工具

在我们日常的外贸工作中&#xff0c;有很多地方都可以用到一些实用外贸小工具去提高工作效率&#xff0c;突破局部限制。是否能够灵活的应用这些实用外贸小工具&#xff0c;是一位成熟优秀外贸业务员的衡量标准之一。第一个&#xff1a;知识信息整理和CRMhttps://www.notion.so…

Linux(六)基础I/O

引言 C语言进阶 文件管理 上一篇文章详细回顾了C语言方面关于文件操作的一些库函数&#xff0c;比如输入输出重定向fscanf、fprintf&#xff0c;对于文件内容以字符形式读取的fgetc、fputc&#xff0c;对于文件内容以字符串形式读取的fgets、fputs&#xff0c;对于二进制文件的…