在前面【native层方法替换热修复原理】之虚拟机运行时数据结构ClassObject/Class和Method/ArtMethod 中我们讲到了2个重要的数据结构ClassObject和Mehod,也提到了这2个数据结构中的一些重要成员,这一节就来看看native方法在dalvik虚拟机中是如何执行的。也就是dalvik虚拟机中JNI模块的实现原理。
我们已经知道jni中native方法包括2种情况:一种是静态注册,一种是动态注册。不清楚的同学可以查看专栏:NDK开发从入门到精通。
这2种方式注册的native函数在dalvik虚拟机中的执行流程是不一样,先来看下动态注册方式的native函数的执行流程。
使用RegisterNatives动态注册的native函数
在安卓NDK开发之JNI函数动态注册中我们讲过native方法的动态注册是通过RegisterNatives这个函数来完成的,因此我们以该函数为切入点来分析下native函数的注册和执行流程。该函数位于安卓源码的dalvik/vm/Jni.cpp。先来看下其定义:
/*
* Register one or more native functions in one class.
*
* This can be called multiple times on the same method, allowing the
* caller to redefine the method implementation at will.
*/
static jint RegisterNatives(JNIEnv* env, jclass jclazz,
const JNINativeMethod* methods, jint nMethods)
{
ScopedJniThreadState ts(env);
ClassObject* clazz = (ClassObject*) dvmDecodeIndirectRef(ts.self(), jclazz);
if (gDvm.verboseJni) {
ALOGI("[Registering JNI native methods for class %s]",
clazz->descriptor);
}
for (int i = 0; i < nMethods; i++) {
if (!dvmRegisterJNIMethod(clazz, methods[i].name,
methods[i].signature, methods[i].fnPtr))
{
return JNI_ERR;
}
}
return JNI_OK;
}
可以看到该函数最重要的逻辑就是调用dvmRegisterJNIMethod函数来完成JNI方法的注册,跟进该函数的实现:
/*
* Register a method that uses JNI calling conventions.
*/
static bool dvmRegisterJNIMethod(ClassObject* clazz, const char* methodName,
const char* signature, void* fnPtr)
{
if (fnPtr == NULL) {
return false;
}
// If a signature starts with a '!', we take that as a sign that the native code doesn't
// need the extra JNI arguments (the JNIEnv* and the jclass).
bool fastJni = false;
if (*signature == '!') {
fastJni = true;
++signature;
ALOGV("fast JNI method %s.%s:%s detected", clazz->descriptor, methodName, signature);
}
Method* method = dvmFindDirectMethodByDescriptor(clazz, methodName, signature);//从direct方法中查找
if (method == NULL) {
method = dvmFindVirtualMethodByDescriptor(clazz, methodName, signature);//从virtual方法中寻找
}
if (method == NULL) {
dumpCandidateMethods(clazz, methodName, signature);
throwNoSuchMethodError(clazz, methodName, signature, "static or non-static");
return false;
}
if (!dvmIsNativeMethod(method)) {//不是native方法直接返回false
ALOGW("Unable to register: not native: %s.%s:%s", clazz->descriptor, methodName, signature);
throwNoSuchMethodError(clazz, methodName, signature, "native");
return false;
}
if (fastJni) {
// In this case, we have extra constraints to check...
if (dvmIsSynchronizedMethod(method)) {
// Synchronization is usually provided by the JNI bridge,
// but we won't have one.
ALOGE("fast JNI method %s.%s:%s cannot be synchronized",
clazz->descriptor, methodName, signature);
return false;
}
if (!dvmIsStaticMethod(method)) {
// There's no real reason for this constraint, but since we won't
// be supplying a JNIEnv* or a jobject 'this', you're effectively
// static anyway, so it seems clearer to say so.
ALOGE("fast JNI method %s.%s:%s cannot be non-static",
clazz->descriptor, methodName, signature);
return false;
}
}
if (method->nativeFunc != dvmResolveNativeMethod) {
/* this is allowed, but unusual */ //从注释这里可以看到Method的nativeFunc字段初始值一般为dvmResolveNativeMethod
ALOGV("Note: %s.%s:%s was already registered", clazz->descriptor, methodName, signature);
}
method->fastJni = fastJni;
dvmUseJNIBridge(method, fnPtr);//前面条件都满足了,调用该方法
ALOGV("JNI-registered %s.%s:%s", clazz->descriptor, methodName, signature);
return true;
}
可以看到该函数处理逻辑于流程是这样的:
- 根据方法的签名在ClassObject对象所代表的类clazz的direct方法区中查找该方法
- 如果在direct方法区中没找到,则在virtual方法区中查找该方法,如果2者都没找到抛出NoSuchMethodError异常
- 如果找到先判断该方法是否是native方法,如果不是打警告日志:不能注册非native方法,然后直接返回false,也就是注册方法失败
- 如果是native方法,使用JNI桥函数dvmUseJNIBridge将该方法所代表的Method数据结构与传入的fnPtr指针指向的c/c++代码中的本地函数建立映射关系(也就是注册)
所以重点在于dvmUseJNIBridge这个函数是如何做到将java层方法对应的Method数据结构与fnPtr指针指向的c/c++代码中的本地函数建立映射关系的?看下该函数的实现:
/*
* Point "method->nativeFunc" at the JNI bridge, and overload "method->insns"
* to point at the actual function.
*/
void dvmUseJNIBridge(Method* method, void* func) {
method->shouldTrace = shouldTrace(method);
// Does the method take any reference arguments?
method->noRef = true;
const char* cp = method->shorty;
while (*++cp != '\0') { // Pre-increment to skip return type.
if (*cp == 'L') {
method->noRef = false;
break;
}
}
DalvikBridgeFunc bridge = gDvmJni.useCheckJni ? dvmCheckCallJNIMethod : dvmCallJNIMethod;
dvmSetNativeFunc(method, bridge, (const u2*) func);
}
从注释上可以看到该函数的作用就是将method->nativeFunc指向JNI桥函数,将method->insns指向实际的函数(point at the actual function)。而完成这个操作的函数是dvmSetNativeFunc,所以我们重点看下该函数是如何实现的(该函数位于安卓源码的dalvik/vm/oo/Class.cpp):
/*
* Replace method->nativeFunc and method->insns with new values. This is
* commonly performed after successful resolution of a native method.
*
* There are three basic states:
* (1) (initial) nativeFunc = dvmResolveNativeMethod, insns = NULL
* (2) (internal native) nativeFunc = <impl>, insns = NULL
* (3) (JNI) nativeFunc = JNI call bridge, insns = <impl>
*
* nativeFunc must never be NULL for a native method.
*
* The most common transitions are (1)->(2) and (1)->(3). The former is
* atomic, since only one field is updated; the latter is not, but since
* dvmResolveNativeMethod ignores the "insns" field we just need to make
* sure the update happens in the correct order.
*
* A transition from (2)->(1) would work fine, but (3)->(1) will not,
* because both fields change. If we did this while a thread was executing
* in the call bridge, we could null out the "insns" field right before
* the bridge tried to call through it. So, once "insns" is set, we do
* not allow it to be cleared. A NULL value for the "insns" argument is
* treated as "do not change existing value".
*/
void dvmSetNativeFunc(Method* method, DalvikBridgeFunc func,
const u2* insns)
{
ClassObject* clazz = method->clazz;
assert(func != NULL);
/* just open up both; easier that way */
dvmLinearReadWrite(clazz->classLoader, clazz->virtualMethods);
dvmLinearReadWrite(clazz->classLoader, clazz->directMethods);
if (insns != NULL) {
/* update both, ensuring that "insns" is observed first */
method->insns = insns;
android_atomic_release_store((int32_t) func,
(volatile int32_t*)(void*) &method->nativeFunc);
} else {
/* only update nativeFunc */
method->nativeFunc = func;
}
dvmLinearReadOnly(clazz->classLoader, clazz->virtualMethods);
dvmLinearReadOnly(clazz->classLoader, clazz->directMethods);
}
从注释上可以看到该函数实际上就是用新的值替换nativeFunc和insns字段(Replace method->nativeFunc and method->insns with new values),从代码实现来看分为2种情况:
- insns不为空,那么2者都更新
- insns为空,仅仅更新 nativeFunc
另外从代码注释上来看insns和nativeFunc的状态转换包括3种情况(There are three basic states):
(1) (initial) nativeFunc = dvmResolveNativeMethod, insns = NULL
(2) (internal native) nativeFunc = <impl>, insns = NULL
(3) (JNI) nativeFunc = JNI call bridge, insns = <impl>第一种是初始状态,nativeFunc字段指向初始值dvmResolveNativeMethod,insns为空
第二种是内部native方法状态:nativeFunc字段指向某个具体的实现,insns为空
第三种是JNI方法状态(也就是开发者在java代码中声明的native方法):nativeFunc字段指向 JNI call bridge,比如通过前面的分析我们已经知道动态注册的native函数在注册过程中会将该字段指向JNI桥函数(dvmCheckCallJNIMethod或dvmCallJNIMethod),insns指向函数的具体实现,也就是注册时传入的fnPtr指针
不管哪种状态,只要该方法是native方法,其nativeFunc字段一定不为NULL。
而JNI桥函数(dvmCheckCallJNIMethod或dvmCallJNIMethod)的作用就是根据传入的Method结构体中保存的参数以及具体实现代码(insns指向的内容)做平台相关的实现。以dvmCallJNIMethod为例,如下一段代码是dvmCallJNIMethod函数中一段重要的调用流程:
void dvmCallJNIMethod(const u4* args, JValue* pResult, const Method* method, Thread* self) {
u4* modArgs = (u4*) args;
...省略若干细节代码...
dvmPlatformInvoke(env,(ClassObject*) staticMethodClass,
method->jniArgInfo, method->insSize, modArgs, method->shorty,
(void*) method->insns, pResult);
...省略若干细节代码...
}
其中的dvmPlatformInvoke就是根据不同平台来实现。
到这里通过动态注册的方式注册的native函数的执行流程就分析完了,先小结下:
- RegisterNatives注册native函数是通过dvmRegisterJNIMethod完成的,该函数会根据注册时传入的函数签名依次从direct,virtual方法区中查找该方法对应的Method对象,查找到之后判断该方法是否为native方法,如果是则将该Method对象和注册时传入的fnPrt指针指向的c/c++层函数建立映射关系
- 将java方法对应的Method对象和c/c++层函数建立映射关系的本质就是用新的值替换nativeFunc和insns字段
- 整个函数调用链是这样的:RegisterNatives->dvmRegisterJNIMethod->dvmUseJNIBridge->dvmSetNativeFunc。这条调用链走完之后就已经将java层声明的native函数和我们在JNI代码中定义的c/c++函数建立起了映射关系,此时Method数据结构中nativeFunc字段指向的值是JNI桥函数(dvmCheckCallJNIMethod或dvmCallJNIMethod),insns指向的值是对应的c/c++中定义的函数的函数地址。
- 等到执行该native函数的时候,本质上是执行该函数的JNI桥函数,在该桥函数中会根据Method数据结构中保存的参数和insns指向的c/c++函数的地址进行执行。这个过程是通过dvmPlatformInvoke函数完成的,该函数是一个和平台相关的调用(实际上从函数名就可以看出来,所以该函数的具体实现应该使用到了类似汇编代码)。
思考:从上面的流程我们能得到一些什么启示?
从上面的流程我们已经知道了native函数在dalvik中的注册流程,也知道了注册的本质就是修改Method数据结构的nativeFunc字段和insns字段,那么知道这些有什么用呢?再进一步想想我们为何要注册native函数?不就是为了在执行一个java层声明的函数时去执行另外一个(c/c++层的)函数吗?调用一个函数实际上去执行另外一个函数不就是劫持或者hook吗?试想下如果我们能够在运行时随意修改一个Mehod对象对应的这2个字段的内容,那么执行该函数时是不是就会去调用我们指定的函数呢?比如将nativeFunc字段指向一个我们自己定义的函数(当然这个函数定义必须满足以下形式)
typedef void (*DalvikBridgeFunc)(const u4* args, JValue* pResult, const Method* method, struct Thread* self);
,在这个函数中我们可以实现一些自己的操作,然后再去调用原函数,这样不就达到类似hook的行为了吗?实际上在后面带领大家分析native层方法替换框架,如Xposed和DexPosed等框架时你会发现他们就是这么做的,换而言之他们借鉴或参考了dalvik虚拟机JNI实现原理的这一套逻辑。
通过命令规则静态注册的native函数
前面分析了通过动态注册方式注册的native函数,接下来分析下静态注册方式注册的native函数的执行流程?在分析之前,我们先来思考一个问题?静态注册和动态注册的native函数的区别在哪里?是代码实现不一样吗?很明显实现是一样的,区别仅仅在于函数的命名是不一样的,同时缺少了动态注册的过程。静态注册函数需要符合命名规则。那么命名会影响什么呢?很显然是函数的查找,所以我在分析之前先预测静态注册函数执行流程的区别和动态注册相比在于函数查找逻辑不一样,换而言之就是得到Method数据结构方式不一样。后面大家可以看我预测的对不对。
对于静态注册方式,因为没有动态注册过程,所以我们需要找另外一个切入点分析,这个切入点就是类加载了之后方法是如何加载及调用的?
通过前面的一系列热修复文章的学习,我们已经知道类加载机制的最终目标就是为目标类生成一个ClassObject数据结构,并将其存储在运行时环境中,为解释器的执行提供相应类方法的字节码。同时在dex中类的加载流程和unexpected DEX异常中已经讲过类的加载过程中ClassObject对象的生成是在findClassNoInit方法中。同时也知道该方法生成ClassObject实际上调用的是loadClassFromDex方法(还不清楚的同学可以先了解下专栏中的热修复相关专题),所以我们以该方法为入口进行分析。该方法位于安卓源码的dalvik\vm\oo\Class.cpp中。因为该方法实际上是调用了loadClassFromDex0方法,所以我们直接分析loadClassFromDex0方法,这个方法非常长,做了很多事,因为我们主要是希望弄明白静态注册的native方法的执行流程。所以省略其他和方法加载无关的代码:
static ClassObject* loadClassFromDex0(DvmDex* pDvmDex,
const DexClassDef* pClassDef, const DexClassDataHeader* pHeader,
const u1* pEncodedData, Object* classLoader)
{
...省略若干细节代码
if (pHeader->directMethodsSize != 0) {//对直接方法进行加载
int count = (int) pHeader->directMethodsSize;
u4 lastIndex = 0;
DexMethod method;
newClass->directMethodCount = count;
newClass->directMethods = (Method*) dvmLinearAlloc(classLoader,
count * sizeof(Method));
for (i = 0; i < count; i++) {
dexReadClassDataMethod(&pEncodedData, &method, &lastIndex);
loadMethodFromDex(newClass, &method, &newClass->directMethods[i]);//加载方法
if (classMapData != NULL) {
const RegisterMap* pMap = dvmRegisterMapGetNext(&classMapData);
if (dvmRegisterMapGetFormat(pMap) != kRegMapFormatNone) {
newClass->directMethods[i].registerMap = pMap;
/* TODO: add rigorous checks */
assert((newClass->directMethods[i].registersSize+7) / 8 ==
newClass->directMethods[i].registerMap->regWidth);
}
}
}
dvmLinearReadOnly(classLoader, newClass->directMethods);
}
if (pHeader->virtualMethodsSize != 0) {//对虚方法进行加载
int count = (int) pHeader->virtualMethodsSize;
u4 lastIndex = 0;
DexMethod method;
newClass->virtualMethodCount = count;
newClass->virtualMethods = (Method*) dvmLinearAlloc(classLoader,
count * sizeof(Method));
for (i = 0; i < count; i++) {
dexReadClassDataMethod(&pEncodedData, &method, &lastIndex);
loadMethodFromDex(newClass, &method, &newClass->virtualMethods[i]);//加载方法
if (classMapData != NULL) {
const RegisterMap* pMap = dvmRegisterMapGetNext(&classMapData);
if (dvmRegisterMapGetFormat(pMap) != kRegMapFormatNone) {
newClass->virtualMethods[i].registerMap = pMap;
/* TODO: add rigorous checks */
assert((newClass->virtualMethods[i].registersSize+7) / 8 ==
newClass->virtualMethods[i].registerMap->regWidth);
}
}
}
...省略若干细节代码
}
从上面可以看到和动态注册类似也分为direct方法和virtual的加载,不管哪种类型最终都是调用loadMethodFromDex方法,跟进该方法:
/*
* Pull the interesting pieces out of a DexMethod.
*
* The DEX file isn't going anywhere, so we don't need to make copies of
* the code area.
*/
static void loadMethodFromDex(ClassObject* clazz, const DexMethod* pDexMethod,
Method* meth)
{
DexFile* pDexFile = clazz->pDvmDex->pDexFile;
const DexMethodId* pMethodId;
const DexCode* pDexCode;
pMethodId = dexGetMethodId(pDexFile, pDexMethod->methodIdx);
meth->name = dexStringById(pDexFile, pMethodId->nameIdx);
dexProtoSetFromMethodId(&meth->prototype, pDexFile, pMethodId);
meth->shorty = dexProtoGetShorty(&meth->prototype);
meth->accessFlags = pDexMethod->accessFlags;
meth->clazz = clazz;
meth->jniArgInfo = 0;
if (dvmCompareNameDescriptorAndMethod("finalize", "()V", meth) == 0) {
/*
* The Enum class declares a "final" finalize() method to
* prevent subclasses from introducing a finalizer. We don't
* want to set the finalizable flag for Enum or its subclasses,
* so we check for it here.
*
* We also want to avoid setting it on Object, but it's easier
* to just strip that out later.
*/
if (clazz->classLoader != NULL ||
strcmp(clazz->descriptor, "Ljava/lang/Enum;") != 0)
{
SET_CLASS_FLAG(clazz, CLASS_ISFINALIZABLE);
}
}
pDexCode = dexGetCode(pDexFile, pDexMethod);
if (pDexCode != NULL) {
/* integer constants, copy over for faster access */
meth->registersSize = pDexCode->registersSize;
meth->insSize = pDexCode->insSize;
meth->outsSize = pDexCode->outsSize;
/* pointer to code area */
meth->insns = pDexCode->insns;
} else {
/*
* We don't have a DexCode block, but we still want to know how
* much space is needed for the arguments (so we don't have to
* compute it later). We also take this opportunity to compute
* JNI argument info.
*
* We do this for abstract methods as well, because we want to
* be able to substitute our exception-throwing "stub" in.
*/
int argsSize = dvmComputeMethodArgsSize(meth);
if (!dvmIsStaticMethod(meth))
argsSize++;
meth->registersSize = meth->insSize = argsSize;
assert(meth->outsSize == 0);
assert(meth->insns == NULL);
if (dvmIsNativeMethod(meth)) { //如果是native方法
meth->nativeFunc = dvmResolveNativeMethod;
meth->jniArgInfo = computeJniArgInfo(&meth->prototype);
}
}
}
这个方法做的事也比较多,因为我们是分析native方法的静态注册,所以重点关注上述代码我注释的地方,可以看到,如果是native方法,那么会将Method对象的nativeFunc成员的值赋为dvmResolveNativeMethod,这也是为何前面提到动态注册native函数时Method对象的nativeFunc成员的初始值是dvmResolveNativeMethod的原因。
有了前面分析动态注册时的经验,我们就知道直接分析该函数即可(该函数位于安卓源码dalvik\vm\Native.cpp目录下)。
/*
* Resolve a native method and invoke it.
*
* This is executed as if it were a native bridge or function. If the
* resolution succeeds, method->insns is replaced, and we don't go through
* here again unless the method is unregistered.
*
* Initializes method's class if necessary.
*
* An exception is thrown on resolution failure.
*
* (This should not be taking "const Method*", because it modifies the
* structure, but the declaration needs to match the DalvikBridgeFunc
* type definition.)
*/
void dvmResolveNativeMethod(const u4* args, JValue* pResult,
const Method* method, Thread* self)
{
ClassObject* clazz = method->clazz;
/*
* If this is a static method, it could be called before the class
* has been initialized.
*/
if (dvmIsStaticMethod(method)) {
if (!dvmIsClassInitialized(clazz) && !dvmInitClass(clazz)) {
assert(dvmCheckException(dvmThreadSelf()));
return;
}
} else {
assert(dvmIsClassInitialized(clazz) ||
dvmIsClassInitializing(clazz));
}
/* start with our internal-native methods */
DalvikNativeFunc infunc = dvmLookupInternalNativeMethod(method); //从内部查找
if (infunc != NULL) {
/* resolution always gets the same answer, so no race here */
IF_LOGVV() {
char* desc = dexProtoCopyMethodDescriptor(&method->prototype);
LOGVV("+++ resolved native %s.%s %s, invoking",
clazz->descriptor, method->name, desc);
free(desc);
}
if (dvmIsSynchronizedMethod(method)) {
ALOGE("ERROR: internal-native can't be declared 'synchronized'");
ALOGE("Failing on %s.%s", method->clazz->descriptor, method->name);
dvmAbort(); // harsh, but this is VM-internal problem
}
DalvikBridgeFunc dfunc = (DalvikBridgeFunc) infunc;
dvmSetNativeFunc((Method*) method, dfunc, NULL);
dfunc(args, pResult, method, self);
return;
}
/* now scan any DLLs we have loaded for JNI signatures */
void* func = lookupSharedLibMethod(method); //从开发者编译的动态库中查找JNI方法
if (func != NULL) {
/* found it, point it at the JNI bridge and then call it */
dvmUseJNIBridge((Method*) method, func); //设置Method->insns字段内容为func
//调用桥函数,注意这里的nativeFunc实际上是JNI桥函数,而不是从动态库中查找到的func
(*method->nativeFunc)(args, pResult, method, self);
return;
}
IF_ALOGW() {
char* desc = dexProtoCopyMethodDescriptor(&method->prototype);
ALOGW("No implementation found for native %s.%s:%s",
clazz->descriptor, method->name, desc);
free(desc);
}
dvmThrowUnsatisfiedLinkError("Native method not found", method);
}
从上面的注释可以看到该函数的作用就是解析一个native函数,然后调用它。如果解析成功
method->insns将被替换(if the resolution succeeds, method->insns is replaced)。从执行流程上来看包括以下几个重要步骤:
- 首先通过dvmLookupInternalNativeMethod函数来查找,对于dalvik虚拟机内部定义的native函数而言,是可以找到的,比如安卓源码dalvik\vm\native路径下定义的各个类中的native方法,
- 对于开发者在java层定义的方法通过lookupSharedLibMethod函数来查找,找到之后返回指向该函数的指针
- 接下来和动态注册时调用流程就差不多了,调用dvmUseJNIBridge将该函数代表的Method对象和前面在动态库中查找到的函数的地址建立映射关系,注意映射关系建立完成之后,Method->insns字段的值为从动态库中查找到的native函数的地址func,Method->nativeFunc的值为JNI桥函数。所以(*method->nativeFunc)(args, pResult, method, self);这一句实际调用的是JNI桥函数。
分析完了之后现在回过头来看我前面的预测,是不是很正确?区别仅仅在于方法的查找逻辑不同,在已经知道java层函数对应的Method对象和native层函数的地址之后,后面建立映射关系的逻辑几乎一模一样。而且可以看到这2种方式查找方法时的侧重点是不同的,静态注册时侧重点在于如何找到该方法对应的native方法的地址,而动态注册查找方法时侧重点在于如何通过注册时传入的函数签名查找到该函数对应的Method对象。原因很简单,因为动态注册是在so加载时注册,此时java层方法调用还未执行,仅仅只是建立java层方法和native层方法的映射关系而已,而且会传入该函数在c/c++层对应的指针,所以native层函数地址是已知条件,而java层函数对应的Method对象是未知条件。而静态注册的执行是在运行时从java层跳到native层时,此时已经发生函数调用,Java层函数对应的Method结构是已知条件,而对应的native层函数是未知条件。
Tips
- 从上面分析可以看到整个过程涉及到了ClassObject和Method数据结构的很多成员,这也是为何前面要对这2个数据结构进行分析的原因,同时也可以感受到数据结构在代码编写中的重要性,函数本质上就是对各种数据结构进行操作和更新。同时也可以看到源码中很多函数都使用了委托调用的形式,即将一个功能很大的函数拆分为n个小功能点的短函数。这也是代码规范的一个技巧。
- 在分析源码时强烈推荐大家用源码阅读工具Source Insight,方便分析流程跳转。
总结
- 不管是静态注册还是动态注册,其目的都是建立起java层方法和native层c/c++方法的映射关系,本质上都是用新的值替换Method数据结构中的nativeFunc和insns字段的值。
- 任何一个native方法,其Method数据结构对应的nativeFunc值一定不为空,在运行过程中这个值一定会指向一个JNI桥函数(dvmCallJNIMethod或者dvmCheckCallJNIMethod),JNI方法的执行就是在该桥函数中被调用执行的
文章评论