Android Dalvik虚拟机JNI方法的注册过程分析

news/2024/5/10 0:40:34

Dalvik虚拟机在调用一个成员函数的时候,如果发现该成员函数是一个JNI方法,那么就会直接跳到它的地址去执行。也就是说,JNI方法是直接在本地操作系统上执行的,而不是由Dalvik虚拟机解释器执行。由此也可看出,JNI方法是Android应用程序与本地操作系统直接进行通信的一个手段。在本文中,我们就详细分析JNI方法的注册过程。

先从System.loadLibrary开始吧…
libcore\luni\src\main\java\java\lang\System.java
在这里插入图片描述
System类的成员函数loadLibrary接下来就再通过运行时类Runtime的成员函数loadLibrary来加载名称为libName的so文件,接下来我们就继续分析它的实现。

frameworks\native\libs\utils\Runtime.java
在这里插入图片描述
参数libraryName只是描述要加载的so文件的部分名称,它的完整名称需要根据本地操作系统的特证来确定。由于目前Android系统都是属于Linux系统,而在Linux系统中,so文件的命名规范通常就是lib.so的形式,这是通过调用System类的静态成员函数mapLibraryName来获得的。

上面所获得的libnanosleep.so文件的名称仍然还不够完整,因为它没有包含绝对路径。在这种情况下,我们是无法将它加载到Dalvik虚拟机中去的。当参数loader的值不等于null的时候,Runtime类的成员函数loadLibrary就会调用它的成员函数findLibrary来它的so文件目录中寻找是否有一外名称为“libnanosleep.so”。如果存在的话,那么就会返回该libnanosleep.so文件的绝对路径。有了libnanosleep.so文件的绝对路径之后,就可以调用Runtime类的另外一个成员函数nativeLoad来将它加载到当前进程的Dalvik虚拟机中。注意,将参数libraryName转换为lib.so的完整形式,以及获得该so文件的绝对路径,都是由参数loader所描述的一个类加载器的成员函数findLibrary来完成的。
另一方面,如果参数loader的值等于null,那么就表示当前要加载的so文件要在系统范围的so文件目录查找。这些系统范围的so文件目录保存在Runtime类的成员变量mLibPaths所描述的一个String数组中。通过依次检查这些目录是否存在与参数libraryName对应的so文件,就可以确定参数libraryName所指定加载的so文件是否是一个合法的so文件。如果合法的话,那么同样会调用Runtime类的另外一个成员函数nativeLoad来将它加载到当前进程的Dalvik虚拟机中。注意,这里在检查参数libraryName所表示的so文件是否存在于系统范围的so文件目录之前,同样要将它转换为lib.so的形式,这同样也是通过调用System类的静态成员函数mapLibraryName来完成的。
如果最后无法在指定的APK或者系统范围的so文件目录中找到由参数libraryName所描述的so文件,或者找到了该so文件,但是在加载该so文件的过程中出现错误,那么Runtime类的成员函数loadLibrary都会抛出一个类型为UnsatisfiedLinkError的异常。

由于加载参数libraryName所描述的so文件是由Runtime类的成员函数nativeLoad来实现的,因此,接下来我们继续分析它的实现。

frameworks\native\libs\utils\Runtime.java
在这里插入图片描述Runtime类的成员函数nativeLoad是一个JNI方法。由于该JNI方法是属于Java核心类Runtime的,也就是说,它在Dalvik虚拟机启动的时候就已经在内部注册过了,因此,这时候我们可以直接调用它注册其它的JNI方法,也就是so文件filename里面所指定的JNI方法。Dalvik虚拟机在启动过程中注册Java核心类的操作,具体可以参考前面Dalvik虚拟机的启动过程分析一文。
Runtime类的成员函数nativeLoad在C++层对应的函数为Dalvik_java_lang_Runtime_nativeLoad,如下所示:

dalvik\vm\ java_lang_Runtime.cpp
在这里插入图片描述
参数args[0]保存的是一个Java层的String对象,这个String对象描述的就是要加载的so文件,函数Dalvik_java_lang_Runtime_nativeLoad首先是调有函数dvmCreateCstrFromString来将它转换成一个C++层的字符串fileName,然后再调用函数dvmLoadNativeCode来执行加载so文件的操作。

接下来,我们就继续分函数dvmLoadNativeCode的实现,以便可以了解一个so文件的加载过程。

dalvik\vm\ Native.cpp
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
函数dvmLoadNativeCode首先是检查参数pathName所指定的so文件是否已经加载过了,这是通过调用函数findSharedLibEntry来实现的。如果已经加载过,那么就可以获得一个SharedLib对象pEntry。这个SharedLib对象pEntry描述了有关参数pathName所指定的so文件的加载信息,例如,上次用来加载它的类加载器和上次的加载结果。如果上次用来加载它的类加载器不等于当前所使用的类加载器,或者上次没有加载成功,那么函数dvmLoadNativeCode就回直接返回false给调用者,表示不能在当前进程中加载参数pathName所描述的so文件。
我们假设参数pathName所指定的so文件还没有被加载过,这时候函数dvmLoadNativeCode就会先调用dlopen来在当前进程中加载它,并且将获得的句柄保存在变量handle中,接着再创建一个SharedLib对象pNewEntry来描述它的加载信息。这个SharedLib对象pNewEntry还会通过函数addSharedLibEntry被缓存起来,以便可以知道当前进程都加载了哪些so文件。
注意,在调用函数addSharedLibEntry来缓存新创建的SharedLib对象pNewEntry的时候,如果得到的返回值pActualEntry指向的不是SharedLib对象pNewEntry,那么就表示另外一个线程也正在加载参数pathName所指定的so文件,并且比当前线程提前加载完成。在这种情况下,函数addSharedLibEntry就什么也不用做而直接返回了。否则的话,函数addSharedLibEntry就要继续负责调用前面所加载的so文件中的一个指定的函数来注册它里面的JNI方法。
这个指定的函数的名称为“JNI_OnLoad”,也就是说,每一个用来实现JNI方法的so文件都应该定义有一个名称为“JNI_OnLoad”的函数,并且这个函数的原型为:
jint JNI_OnLoad(JavaVM* vm, void* reserved)
函数dvmLoadNativeCode通过调用函数dlsym就可以获得在前面加载的so中名称为“JNI_OnLoad”的函数的地址,最终保存在函数指针func中。有了这个函数指针之后,我们就可以直接调用它来执行注册JNI方法的操作了。注意,在调用该JNI_OnLoad函数时,第一个要传递进行的参数是一个JavaVM对象,这个JavaVM对象描述的是在当前进程中运行的Dalvik虚拟机,第二个要传递的参数可以设置为NULL,这是保留给以后使用的。
从前面Dalvik虚拟机的启动过程分析一文可以知道,在当前进程所运行的Dalvik虚拟机实例是通过全局变量gDvm所描述的一个DvmGlobals结构体的成员变量vmList来描述的,因此,我们就可以将它传递在前面加载的so中名称中定义的JNI_OnLoad函数。注意,定义在该so文件中的JNI_OnLoad函数一旦执行成功,它的返回值就必须等于JNI_VERSION_1_2、JNI_VERSION_1_4或者JNI_VERSION_1_6,用来表示所注册的JNI方法的版本。
最后,函数dvmLoadNativeCode根据上述的JNI_OnLoad函数的执行成功与否,将前面所创建的一个SharedLib对象pNewEntry的成员变量onLoadResult设置为kOnLoadOkay或者kOnLoadFailed,这样就可以记录参数pathName所指定的so文件是否是加载成功的,也就是它是否成功地注册了其内部的JNI方法。
在我们这个情景中,参数pathName所指定的so文件为libnanosleep.so,接下来我们就继续分析它的函数JNI_OnLoad的实现,以便可以发解定义在它里面的JNI方法的注册过程。

定义在libnanosleep.so文件中的函数JNI_OnLoad的实现可以参考文章开始的部分。通过调用函数jniRegisterNativeMethods来实现的。因此,接下来我们就继续分析函数jniRegisterNativeMethods的实现。

Libnativehelper\ JNIHelp.cpp
在这里插入图片描述
参数env所指向的一个JNIEnv结构体,通过调用这个JNIEnv结构体可以获得参数className所描述的一个类。这个类就是要注册JNI的类,而它所要注册的JNI就是由参数gMethods来描述的。
注册参数gMethods所描述的JNI方法是通过调用env所指向的一个JNIEnv结构体的成员函数RegisterNatives来实现的,因此,接下来我们就继续分析它的实现。

dalvik\vm\ Jni.cpp
在这里插入图片描述
参数jclazz描述的是要注册JNI方法的类,而参数methods描述的是要注册的一组JNI方法,这个组JNI方法的个数由参数nMethods来描述。
函数RegisterNatives首先是调用函数dvmDecodeIndirectRef来获得要注册JNI方法的类对象,接着再通过一个for循环来依次调用函数dvmRegisterJNIMethod注册参数methods描述所描述的每一个JNI方法。注意,每一个JNI方法都由名称、签名和地址来描述。
接下来,我们就继续分析函数dvmRegisterJNIMethod的实现。

dalvik\vm\ Jni.cpp
在这里插入图片描述
在这里插入图片描述
一个JNI方法并不是直接被调用的,而是通过由Dalvik虚拟机间接地调用,这个用来间接调用JNI方法的函数就称为一个Bridge。这些Bridage函数在真正调用JNI方法之前,会执行一些通用的初始化工作。例如,会将当前线程的状态设置为NATIVE,因为它即将要执行一个Native函数。又如,会为即将要被调用的JNI方法准备好前面两个参数,第一个参数是一个JNIEnv对象,用来描述当前线程的Java环境,通过它可以访问反过来访问Java代码和Java对象,第二个参数是一个jobject对象,用来描述当前正在执行JNI方法的Java对象。
这些Bridage函数实际上仍然不是直接调用地调用JNI方法的,这是因为Dalvik虚拟机是可以运行在各种不同的平台之上,而每一种平台可能都定义有自己的一套函数调用规范,也就是所谓的ABI(Application Binary Interface),这是一个API(Application Programming Interface)不同的概念。ABI是在二进制级别上定义的一套函数调用规范,例如参数是通过寄存器来传递还是堆栈来传递,而API定义是一个应用程序编程接口规范。换句话说,API定义了源代码和库之间的接口,因此同样的代码可以在支持这个API的任何系统中编译 ,而ABI允许编译好的目标代码在使用兼容ABI的系统中无需改动就能运行。
为了使得运行在不同平台上的Dalvik虚拟机能够以统一的方法来调用JNI方法,这些Bridage函数使用了一个libffi库,它的源代码位于external/libffi目录中。Libffi是一个开源项目,用于高级语言之间的相互调用的处理,它的实现机制可以进一步参考http://www.sourceware.org/libffi/。
回到函数dvmUseJNIBridge中,它主要就是根据Dalvik虚拟机的启动选项来为即将要注册的JNI选择一个合适的Bridge函数。如果我们在Dalvik虚拟机启动的时候,通过-Xjnitrace选项来指定了要跟踪参数method所描述的JNI方法,那么函数dvmUseJNIBridge为该JNI方法选择的Bridge函数就为dvmTraceCallJNIMethod,否则的话,就再通过另外一个函数dvmSelectJNIBridge来进一步选择一个合适的Bridge函数。选择好Bridge函数之后,函数dvmUseJNIBridge最终就调用函数dvmSetNativeFunc来执行真正的JNI方法注册操作。接着再分析函数dvmSetNativeFunc的实现。

dalvik\vm\oo\ Class.cpp
在这里插入图片描述
参数method表示要注册JNI方法的Java类成员函数,参数func表示JNI方法的Bridge函数,参数insns表示要注册的JNI方法的函数地址。
当参数insns的值不等于NULL的时候,函数dvmSetNativeFunc就分别将参数insns和func的值分别保存在参数method所指向的一个Method对象的成员变量insns和nativeFunc中,而当insns的值等于NULL的时候,函数dvmSetNativeFunc就只将参数func的值保存在参数method所指向的一个Method对象成员变量nativeFunc中。
假设在前面的Step 11中选择的Bridge函数为dvmCallJNIMethod_general,并且结合前面Dalvik虚拟机的运行过程分析一文,我们就可以得到Dalvik虚拟机在运行过程中调用JNI方法的过程:

  1. 调用函数dvmCallJNIMethod_general,执行一些必要的准备工作;
  2. 函数dvmCallJNIMethod_general再调用函数dvmPlatformInvoke来以统一的方式来调用对应的JNI方法;
  3. 函数dvmPlatformInvoke通过libffi库来调用对应的JNI方法,以屏蔽Dalvik虚拟机运行在不同目标平台的细节。
    至此,我们就分析完成Dalvik虚拟机JNI方法的注册过程了。这样,我们就打通了Java代码和Native代码之间的道路。实际上,很多Java和Android核心类的功能都是通过本地操作系统提供的系统调用来完成的,例如,Zygote类的成员函数forkAndSpecialize最终是通过Linux系统调用fork来创建一个Android应用程序进程的,又如,Thread类的成员函数start最终是通过pthread线程库函数pthread_create来创建一个Android应用程序线程的。

http://www.mrgr.cn/p/11186078

相关文章

STM32H750时钟频率和功耗以及RTC功能测试

STM32H750时钟频率和功耗和RTC功能测试 📌相关篇《STM32H750片外QSPI启动配置简要》 ✨在使用STM32CubeMX修改STM32H750时钟树参数时,如果使用软件自动求解,这是一个非常耗时的操作,有时候还不一定成功,还是推荐使用手…

数据库中间件-He3Proxy

什么是数据库中间件? 随着互联网行业的蓬勃发展,业务访问量、数据量激增,传统数据库的单库、大表已成为业务发展的瓶颈,进而衍生出数据库主从实例、分库分表等方案,为减少数据库层变动对业务开发带来的复杂性,一种连接应用与数据库桥梁的工具孕育而生,即数据库中间件,它…

JAVA前端快速入门基础_javascript入门(01)

写在前面:本文用于快速学会简易的JS,仅做扫盲和参考作用 1.JS是什么 JavaScript是一门跨平台,面向对象的脚本语言(即不需要编译,可以直接通过浏览器进行解释)。JS和Java是两门完全不相同的语言,但是基础的语法是类似的 2.JS的引…

MySQL学习之explain

from之后的查询得到的表叫做衍生表,是临时表数据,生成临时表之后的数据是无法使用索引的,如果数据量大查询效率就会比较低,这就是查询要尽量少使用子查询这些临时表。explain详解 id: 表示查询序号,也可以表示优先级;当值都不一样的时候,值越大表示优先级越高,越先执行…

实验12-使用keras预训练模型完成猫狗识别

版本python3.7 tensorflow版本为tensorflow-gpu版本2.6 运行结果: 这里我用Gpu进行加速,训练一回9秒,如果不启用gpu,训练一回会很慢。 代码:#-*- codeing = utf-8 -*- #@Time : 2022/10/2 11:44 #@Author : 程浩 #@File : 猫狗识别.py #@Software: PyCharm import tensor…

KVM虚拟机迁移(静态)

1.查看虚拟机状态,确认关闭状态 virsh list --all 2.查看虚拟机文件位置 virsh domblklist zabbix3.导出配置文件并查看导出文件 virsh dumpxml zabbix > /root/zabbix.xml 4.把刚导出的配置文件传到目的服务制定路径(路径为虚拟机配置文件位置)scp zabbix.xml 10.10.7.1…

实验11-使用keras完成逻辑回归

版本python3.7 tensorflow版本为tensorflow-gpu版本2.6 运行结果: 代码:import numpy as npfrom keras.models import Sequential from keras.layers import Dense, Dropout, Activation, Flatten import matplotlib.pyplot as plt from sklearn import datasets# 样本数据集…

【工具-PyCharm】

工具-PyCharm ■ PyCharm-简介■ PyCharm-安装■ PyCharm-使用■ 修改主题■ 设置字体■ 代码模板■ 解释器配置■ 文件默认编码■ 快捷键■ 折叠■ 移动■ 注释■ 编辑■ 删除■ 查看■ 缩进■ 替换 ■ PyCharm-简介 官方下载地址 Professional:专业版&#xff0…

Linux KASAN使用与实现原理

一、KASAN工具使用 KASAN工具:Kernel Address SANitizer(KASAN)是一种动态内存安全错误检测工具,主要功能是检查内存越界访问和使用已释放内存的问题。 1.1 KASAN宏控开关 KASAN有三种模式:1.通用KASAN;2.基于软件标签的KASAN&…

实验8-1tensorboard可视化+实验8-2tensorboard案例

版本python3.7 tensorflow版本为tensorflow-gpu版本2.6 实验8-1tensorboard可视化运行结果:代码:import tensorflow as tf# 创建默认图 graph = tf.compat.v1.get_default_graph()# 定义命名空间 with graph.as_default():with tf.name_scope(input):# fetch:就是同时运行多…

科技论文网站:中国科技论文在线

文章目录 1. Intro2. Main3. Cons Evaluation彩蛋:科学素质 这是作者最后一次发 这种类型的宣传 科普文章 1. Intro 中国科技论文在线是经教育部批准,由教育部科技发展中心主办, 利用现代信息技术手段,打破传统出版物的概念&…

虚方法

若一个实例方法声明前带有virtual关键字,那么这个方法就是虚方法。虚方法与非虚方法的最大不同是,虚方法的实现可以由派生类所取代,这种取代是通过方法的重写实现的(以后再讲)虚方法的特点:虚方法前不允许有static,abstract,或override修饰符虚方法不能是私有的,因此不能…

【Vue】如何使用Webpack实现打包操作

一、Webpack介绍 Webpack最主要的作用就是打包操作,由两个核心部分构成分别是“出口”与“入口”。wbepack是现在比较热门的打包工具了,它可以将许多松散耦合的模块按照依赖和规则打包成符合生产环境部署的前端资源。说的直白一点,通过webpac…

RPC(远程过程调用)详解

一、RPC是什么 RPC是指远程过程调用,也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。 二、RPC需要解决的问题 1、Call ID映射 我们怎么告诉远…

【基础算法总结】双指针算法二

双指针 1.有效三角形的个数2.和为S的两个数字3.和为S的两个数字4.四数之和 点赞👍👍收藏🌟🌟关注💖💖 你的支持是对我最大的鼓励,我们一起努力吧!😃😃 1.有效三角形的个数…

前端工程化Vue使用Node.js设置国内高速npm镜像源(踩坑记录版)

前端工程化Vue使用Node.js设置国内高速npm镜像源(踩坑记录版) 此篇仅为踩坑记录,并未成功更换高速镜像源,实际解决方法见文末跳转链接。 1.自身源镜像 自身镜像源创建Vue项目下载速度感人 2.更改镜像源 2.1 通过命令行配置 前提…

京东web端h5st—4.7逆向分析

声明 本文章中所有内容仅供学习交流,抓包内容、敏感网址、数据接口均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关,若有侵权,请联系我立即删除! 目标网站 aHR0cHM6Ly93d3cuamQuY29tLw== 分析流程了解h5st 看了sha256相关加密算法逻辑b…

API和微服务设计的优化方式有哪些?

在构建响应迅速、用户体验良好的应用程序中,API性能的优化至关重要。在构建高性能的API时,采取综合策略是至关重要的。通过采用一系列策略,我们可以确保API在处理请求时高效运行,提供流畅的服务。 一、API和微服务设计的优化可以…

XYCTF2024-web-wp

怎么全是傻逼绕过题。 不想评价,就随便打着玩,除了最后一道java反序列化搞心态,其他的ak了:简单题不想说,http注意一下代理是用Via就行,warmup直接:http://xyctf.top:37034/?val1=240610708&val2=QNKCDZO&md5=0e215962017&XYCTF=240610708&XY=24061070…

深入mysql索引

1. 索引 索引是对数据库表中一列或多列的值进行排序的一种结构。 MySQL索引的建立对于MySQL的高效运行是很重要的,索引可以大大提高MySQL的检索速度。索引只是提高效率的一个因素,如果你的MySQL有大数据量的表,就需要花时间研究建立最优秀的索引,或优化查询语句。 简单类比…