搞Android的工程师对于AndFix大家都应该耳熟能详了,我就不多做介绍了,下面简单的介绍下我接触过的AndFix的各个版本方法替换的大概原理
- 第一个版本的AndFix分为Dalvik与ART两个大的替换方向只要是针对于Android虚拟机来做的,Dalvik更多是基于解释执行的,ART如果开启了AOP的话那么思路会有所不同
Dalvik的方法替换的核心代码:
extern void __attribute__ ((visibility ("hidden"))) dalvik_replaceMethod(
JNIEnv* env, jobject src, jobject dest) {
jobject clazz = env->CallObjectMethod(dest, jClassMethod);
ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(
dvmThreadSelf_fnPtr(), clazz);
clz->status = CLASS_INITIALIZED;
Method* meth = (Method*) env->FromReflectedMethod(src);
Method* target = (Method*) env->FromReflectedMethod(dest);
LOGD("dalvikMethod: %s", meth->name);
meth->jniArgInfo = 0x80000000;
meth->accessFlags |= ACC_NATIVE; // 把替换的方法变成为Native方法
int argsSize = dvmComputeMethodArgsSize_fnPtr(meth);
if (!dvmIsStaticMethod(meth))
argsSize++;
meth->registersSize = meth->insSize = argsSize;
meth->insns = (void*) target;
meth->nativeFunc = dalvik_dispatcher; //替换掉nativeFunc的函数
}
extern void dalvik_setFieldFlag(JNIEnv* env, jobject field) {
Field* dalvikField = (Field*) env->FromReflectedField(field);
dalvikField->accessFlags = dalvikField->accessFlags & (~ACC_PRIVATE)
| ACC_PUBLIC;
LOGD("dalvik_setFieldFlag: %d ", dalvikField->accessFlags);
}
static bool dvmIsPrimitiveClass(const ClassObject* clazz) {
return clazz->primitiveType != PRIM_NOT;
}
static void dalvik_dispatcher(const u4* args, jvalue* pResult,
const Method* method, void* self) {
ClassObject* returnType;
jvalue result;
ArrayObject* argArray;
LOGD("dalvik_dispatcher source method: %s %s", method->name,
method->shorty);
Method* meth = (Method*) method->insns;
meth->accessFlags = meth->accessFlags | ACC_PUBLIC;
LOGD("dalvik_dispatcher target method: %s %s", method->name,
method->shorty);
returnType = dvmGetBoxedReturnType_fnPtr(method);
if (returnType == NULL) {
assert(dvmCheckException_fnPtr(self));
goto bail;
}
LOGD("dalvik_dispatcher start call->");
if (!dvmIsStaticMethod(meth)) {
Object* thisObj = (Object*) args[0];
ClassObject* tmp = thisObj->clazz;
thisObj->clazz = meth->clazz;
argArray = boxMethodArgs(meth, args + 1);
if (dvmCheckException_fnPtr(self))
goto bail;
dvmCallMethod_fnPtr(self, (Method*) jInvokeMethod,
dvmCreateReflectMethodObject_fnPtr(meth), &result, thisObj,
argArray);
thisObj->clazz = tmp;
} else {
argArray = boxMethodArgs(meth, args);
if (dvmCheckException_fnPtr(self))
goto bail;
dvmCallMethod_fnPtr(self, (Method*) jInvokeMethod,
dvmCreateReflectMethodObject_fnPtr(meth), &result, NULL,
argArray);
}
if (dvmCheckException_fnPtr(self)) {
Object* excep = dvmGetException_fnPtr(self);
jni_env->Throw((jthrowable) excep);
goto bail;
}
if (returnType->primitiveType == PRIM_VOID) {
LOGD("+++ ignoring return to void");
} else if (result.l == NULL) {
if (dvmIsPrimitiveClass(returnType)) {
jni_env->ThrowNew(NPEClazz, "null result when primitive expected");
goto bail;
}
pResult->l = NULL;
} else {
if (!dvmUnboxPrimitive_fnPtr(result.l, returnType, pResult)) {
char msg[1024] = { 0 };
snprintf(msg, sizeof(msg) - 1, "%s!=%s\0",
((Object*) result.l)->clazz->descriptor,
returnType->descriptor);
jni_env->ThrowNew(CastEClazz, msg);
goto bail;
}
}
bail: dvmReleaseTrackedAlloc_fnPtr((Object*) argArray, self);
}
首先要做的是拿到Dalvik虚拟机的暴露出来的可以被操作的函数(通过dlopen与dvm_dlsym来进行操作熟悉Linux动态库的应该知道),举个例子:
void* dvm_hand = dlopen("libdvm.so", RTLD_NOW);
if (dvm_hand) {
dvmCallMethod_fnPtr = dvm_dlsym(dvm_hand,
apilevel > 10 ?
"_Z13dvmCallMethodP6ThreadPK6MethodP6ObjectP6JValuez" :
"dvmCallMethod");
//拿到了虚拟机中的dvmCallMethod方法,此方法可以作为动态调用的基础
jInvokeMethod = env->GetMethodID(clazz, "invoke",
"(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;");
//这里和虚拟机没关系,这里是通过JNI拿到了反射的invoke方法为下面做准备
其次是把要被替换的方法变为Native方法,因为由于虚拟机的执行顺序如果是JAVA方法会被解释执行(解释执行里面的字节码指令),如果是Native方法那么就会直接调用(Method 结构体里面的nativeFunc就是函数的地址),这里再覆盖他的nativeFunc为自定义的dalvik_dispatcher方法,这里面最核心的就是dvmCallMethod_fnPtr这个指针函数的调用,
dvmCallMethod_fnPtr(self, (Method*) jInvokeMethod,
dvmCreateReflectMethodObject_fnPtr(meth), &result, thisObj,argArray);
这里通过这个JVM函数动态调用了JAVA的invoke的反射方法,方法体就是替换的函数内容dvmCreateReflectMethodObject_fnPtr(meth), 总结就是让被替换方法走Native的方式然后在JNI层去Hook住nativeFun,然后操作JVM的调用函数去调用反射的方式去执行一个新的JAVA方法
- ART的方法替换核心代码:
art::mirror::ArtMethod* smeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(src);
art::mirror::ArtMethod* dmeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(dest);
dmeth->declaring_class_->class_loader_ =
smeth->declaring_class_->class_loader_; //for plugin classloader
dmeth->declaring_class_->clinit_thread_id_ =
smeth->declaring_class_->clinit_thread_id_;
dmeth->declaring_class_->status_ = smeth->declaring_class_->status_-1;
smeth->declaring_class_ = dmeth->declaring_class_;
smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;
smeth->access_flags_ = dmeth->access_flags_;
smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;
smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;
smeth->method_index_ = dmeth->method_index_;
smeth->dex_method_index_ = dmeth->dex_method_index_;
smeth->ptr_sized_fields_.entry_point_from_interpreter_ =
dmeth->ptr_sized_fields_.entry_point_from_interpreter_;
smeth->ptr_sized_fields_.entry_point_from_jni_ =
dmeth->ptr_sized_fields_.entry_point_from_jni_;
smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =
dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;
LOGD("replace_6_0: %d , %d",
smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_,
dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_);
看起来非常简单,就是在JNI层去找到被替换的与替换的ArtMethod* 结构体(每一个Java方法在art中都对应着一个ArtMethod),然后把这个结构体里面的内容全部替换就OK了就是这么简单,然后整个函数就会被替换了。
- 由于ART的方法非常的简单,所以新版本的AndFix中吸取了里面的精华部分,新版本的AndFix也分为Dalvik,ART两个方式
Dalvik的方法替换的核心代码:
extern void __attribute__ ((visibility ("hidden"))) dalvik_replaceMethod(
JNIEnv* env, jobject src, jobject dest) {
jobject clazz = env->CallObjectMethod(dest, jClassMethod);
ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(
dvmThreadSelf_fnPtr(), clazz);
clz->status = CLASS_INITIALIZED;
Method* meth = (Method*) env->FromReflectedMethod(src);
Method* target = (Method*) env->FromReflectedMethod(dest);
LOGD("dalvikMethod: %s", meth->name);
// meth->clazz = target->clazz;
meth->accessFlags |= ACC_PUBLIC;
meth->methodIndex = target->methodIndex;
meth->jniArgInfo = target->jniArgInfo;
meth->registersSize = target->registersSize;
meth->outsSize = target->outsSize;
meth->insSize = target->insSize;
meth->prototype = target->prototype;
meth->insns = target->insns;
meth->nativeFunc = target->nativeFunc;
}
就问你服没?,在JNI层里面获取被替换方法以及替换方法的Method(Method在Dalvik里面代表一个JAVA方法的结构体),然后整体替换里面的内容即可,这里面注意
meth->insns = target->insns;
meth->nativeFunc = target->nativeFunc;
是不是又看见这个nativeFunc了?但是思路不同了这两句指的是如果是普通JAVA方法那么insns替换,如果是native方法那么nativeFunc替换原理上面应该是JAVA,Native方法替换都支持的
- 新版本的ART代码没有什么改变,
void replace_7_0(JNIEnv* env, jobject src, jobject dest) {
art::mirror::ArtMethod* smeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(src);
art::mirror::ArtMethod* dmeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(dest);
// reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->class_loader_ =
// reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->class_loader_; //for plugin classloader
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->clinit_thread_id_ =
reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->clinit_thread_id_;
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->status_ =
reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->status_ -1;
//for reflection invoke
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->super_class_ = 0;
smeth->declaring_class_ = dmeth->declaring_class_;
smeth->access_flags_ = dmeth->access_flags_ | 0x0001;
smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;
smeth->dex_method_index_ = dmeth->dex_method_index_;
smeth->method_index_ = dmeth->method_index_;
smeth->hotness_count_ = dmeth->hotness_count_;
smeth->ptr_sized_fields_.dex_cache_resolved_methods_ =
dmeth->ptr_sized_fields_.dex_cache_resolved_methods_;
smeth->ptr_sized_fields_.dex_cache_resolved_types_ =
dmeth->ptr_sized_fields_.dex_cache_resolved_types_;
smeth->ptr_sized_fields_.entry_point_from_jni_ =
dmeth->ptr_sized_fields_.entry_point_from_jni_;
smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =
dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;
LOGD("replace_7_0: %d , %d",
smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_,
dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_);
}
但是有一个问题
![](https://img.haomeiwen.com/i9387913/39437676793623bf.png)
看见没只支持到7.0,这个是为什么呢?
因为Andfix是把底层结构强转为了art::mirror::ArtMethod,但这里的art::mirror::ArtMethod并非等同于app运行时所在设备虚拟机底层的art::mirror::ArtMethod,而是Andfix自己构造的art::mirror::ArtMethod(就是上图对应的那些头文件里面的结构体)
但是由于Android是开源的,各个手机厂商都可以对代码进行改造,而Andfix里ArtMethod的结构是根据公开的Android源码中的结构写死的。如果某个厂商对这个ArtMethod结构体进行了修改,就和原先开源代码里的结构不一致,那么在这个修改过了的设备上,替换机制就会出问题。
而且大家也看到了AndFix现在处于不维护的状态了(Android都出到了10了),那么怎么办呢,我在Sophix上面找到了方案(这是铁了心要抛弃Andfix了?)
![](https://img.haomeiwen.com/i9387913/72ab004cfbce9d6b.png)
怎么整体替换呢?在art里面初始化一个类的时候会给这个类的所有方法分配空间
有direct方法和virtual方法(direct方法包含static方法和所有不可继承的对象方法。而virtual方法就是所有可以继承的对象方法了)就是利用ART虚拟机中分配类方法的的地址是连续Alloc出来的,那么只要计算两个方法的地址空间就能得出一个ArtMethod* 结构体的size,得到这个size就利用memcpy整体拷贝不就OK了是吗?
size_t firMid = (size_t) env->GetStaticMethodID(nativeStructsModelClazz, "f1", "()V");
size_t secMid = (size_t) env->GetStaticMethodID(nativeStructsModelClazz, "f2", "()V");
size_t methSize = secMid - firMid;
memcpy(smeth, dmeth, methSize);
值得一提的是,由于忽略了底层ArtMethod结构的差异,对于所有的Android版本都不再需要区分,而统一以memcpy实现即可,代码量大大减少。即使以后的Android版本不断修改ArtMethod的成员,只要保证ArtMethod数组仍是以线性结构排列,就能直接适用于Android 8.0、9.0等新版本,无需再针对新的系统版本进行适配了。
有耐心的小伙伴可以参考这篇文章:https://yq.aliyun.com/articles/74598?t=t1
最后大家也看出来了,方法替换核心还是在JVM上面动手脚,如果想再深入的话首先要对于JVM有足够多的了解,大家加油···
网友评论