您的位置  > 互联网

插件工程遇到诡异的JNI链接错误,你知道吗?

插件工程具有减少方法数量和封装尺寸、易于扩展的优点。 受到大型项目的青睐。 然而,插件工程也会带来一些意想不到的麻烦。 我们最近在我们正在开发的插件项目中遇到了一个奇怪的 JNI 链接错误。

我们的插件项目就是主项目的具体业务。 主项目提供了基本的类库和工具。 插件项目有自己的项目,并将主项目设置为其自己的父项目。 通过双亲委派,插件项目可以访问主项目。 班级在 . 主工程中有一个带有JNI方法的类库,但是为了减少主工程的包大小,so文件在使用时是由插件自己下载加载的。

而且这个加载方法有一个奇怪的错误。 我们首先检查了.load方法,发现没有错误。 然后我们查看进程的内存映射信息,发现so文件确实已经加载了,但是调用JNI方法时确实总是出错。 检查了时序等相关情况后,仍然失败,所以我们只好求助于系统源码,希望从源码中找到答案。 以N为例,我们开始源码分析过程。

so加载过程分析

既然so必须先加载才能使用,所以我们先看看so是如何加载的,先分析一下.load方法。

在这里写下图片描述

方法非常简单。 直接调用类的load方法,传入so的名称和当前的so名称。 我们再看一下这个方法。

在这里写下图片描述

可以看到,load在验证参数后调用该方法,在获得sum后调用layer函数。 继续看函数。

在这里写下图片描述

这仍然是一个非常简单的功能。 设置好之后,继续看要调用的函数。

在这里写下图片描述

在这里写下图片描述

这个函数虽然长,但是逻辑还是很清晰的。 我们只列出关键代码,保存一个包含so路径和对象的Map作为记录,并保存当前加载的所有so。 首先,从中搜索记录。 如果有说明so已加载,则判断与so关联的是否是当前的。 如果不是,则返回false,表示同一个路径的so只能加载一次。 如果没有找到记录,则解释如果so还没有加载,则打开so,将相关信息保存到对象中,添加到其中,使用dlsym查找函数,如果找到,则执行该函数。 我看代码的时候第一反应是判断这里是不是有问题,所以已经被另外一个加载了,但是转念一想,如果这里有问题的话,那么就直接报错了加载,而不是在执行过程中报告错误。 。 所以so的加载过程中没有发现有问题的点,所以我们再看一下执行过程。

方法执行过程分析

我们知道,在ART环境中,类方法会用 来表示,字段中存储的是方法的跳转地址。

在这里写下图片描述

其中_为函数执行时的跳转地址。 那么这个地址是什么呢? 事实上,这个地址是在Class加载时设置的。 我们看一下代码:

在这里写下图片描述

ART中负责加载Class,会通过->->->进行解析,最后通过跳转地址进行赋值。 这里我们只看方法,执行函数。

在这里写下图片描述

这意味着给_赋值。 通过b()获取该值,即ab函数地址。 此时我们知道,类加载后,其方法地址被设置为入口函数b。 当该方法执行时,该入口点将被调用。 函数执行,我们看一下这个函数。

在这里写下图片描述

_stub 在汇编中定义,并且与平台相关。 我们以arm64平台代码为例。

在这里写下图片描述

在这里写下图片描述

可以看到这个函数又跳转到了该函数。

在这里写下图片描述

该函数首先查询函数的地址。 找到之后会通过设置给出,这样以后就可以直接跳转到该层的地址,不用每次都经过函数。 也称为设置跳转地址。 接下来看hod函数。

在这里写下图片描述

在这里我们又看到了一些熟悉的东西。 从前面so加载部分的分析我们已经知道它保存了所有加载的so,所以这里就是从加载的so中查找函数。 如果没有找到,就会抛出。 我们再看一下。

在这里写下图片描述

在这里写下图片描述

就是调用dlsym来获取函数的地址,所以实际上是找到了函数的地址,但是我们注意到其中一个判断,->()==er,即与so关联的一定是和现在的一样。 否则,我们将放弃寻找。 至此,我们的疑惑就解决了,因为JAVA层的代码在主项目中,而使用插件来加载so。 两者不相等,所以我们在这里放弃了。 搜索抛出异常。

解决方案

一旦知道了原因,解决起来就很容易了。 只需使用相同的加载类等等。 因为Java层无法更改,所以我们将so的加载进行更改: 1、使用主工程中的类来加载so。 2.如果在主项目中添加代码有困难,我们还可以更改插件中使用的.load()。 但是load方法只有一个公共参数,并且传递的方法是私有的,所以我们只能使用反射。 转到主项目。

在这里写下图片描述

附:Java基础反射