Xposed framework
是一个基于Android
系统实现的能够给用户提供修改系统层面或第三方APP功能的框架服务。Android
中有一个叫做Zygote
的核心进程,它会随Android
系统的启动而启动,然后加载系统所需的类,最后再调用初始化方法。每一个APP的进程都是从Zygote
进程fork
出的子进程,这个进程的文件是/system/bin/app_process
。 Xposed framework
后,Xposed framework
会替换一个新的app_process
至/system/bin/
中,同时还会替换虚拟机和其他若干文件。Zygote
启动时,会加载Xposed
所需的JAR
包(/data/data/de.robv.android.xposed.installer/bin/XposedBridge.jar
)至系统目录,并启动Xposed
替换的虚拟机。 Xposed framework
的主要接口由XposedBridge.jar
提供,框架的核心功能在替换的虚拟机中实现。用户可以使用Xposed framework
去Hook
方法,下面是作者本人给出的一个示例:
package de.robv.android.xposed.mods.tutorial;
import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
import android.graphics.Color;
import android.widget.TextView;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;
public class Tutorial implements IXposedHookLoadPackage {
public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
if (!lpparam.packageName.equals("com.android.systemui"))
return;
findAndHookMethod("com.android.systemui.statusbar.policy.Clock", lpparam.classLoader, "updateClock", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
TextView tv = (TextView) param.thisObject;
String text = tv.getText().toString();
tv.setText(text + " :)");
tv.setTextColor(Color.RED);
}
});
}
}
这个示例代码实现了对Andoird
系统时钟输出样子和颜色的修改,完整的源码地址是https://github.com/rovo89/XposedExamples/tree/master/RedClock。如此简单的几行代码,它的内部实现机制是怎么的呢?想知道这些,最好的办法当然是分析源码。
这里使用Xposed framework
Hook
java.net.URLEncoder
类的encode
方法作为例子分析,调用逻辑如下所示:
public class Tutorial implements IXposedHookLoadPackage {
public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
findAndHookMethod("java.net.URLEncoder", lpparam.classLoader, "encode", String.class, String.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
}
});
handleLoadPackage
会在任意一个APP加载的时候被调用,然后就可以在这个接口内部根据包名做逻辑判断,如果是Hook
方法所在的包,则进一步调用findAndHookMethod
去Hook
指定的方法。
通过findAndHookMethod()
接口找到指定方法并进行Hook
。它的参数较多,第一个参数是类名,第二个参数是类加载器,第三个参数是方法名,后面是可变参数,把方法的参数类型依次作为参数,最后一个参数是XC_MethodHook()
对象。另外,在其内部重写beforeHookedMethod
和afterHookedMethod
方法,这两个方法分别会在Hook
方法执行的前后执行,用户可以在其中实现自己的逻辑。
findAndHookMethod
这个接口的实现在XposedHelpers.java
这个文件中,代码如下:
public static XC_MethodHook.Unhook findAndHookMethod(String className, ClassLoader classLoader, String methodName, Object... parameterTypesAndCallback) {
return findAndHookMethod(findClass(className, classLoader), methodName, parameterTypesAndCallback);
}
public static XC_MethodHook.Unhook findAndHookMethod(Class> clazz, String methodName, Object... parameterTypesAndCallback) {
if (parameterTypesAndCallback.length == 0 || !(parameterTypesAndCallback[parameterTypesAndCallback.length-1] instanceof XC_MethodHook))
throw new IllegalArgumentException("no callback defined");
XC_MethodHook callback = (XC_MethodHook) parameterTypesAndCallback[parameterTypesAndCallback.length-1];
Method m = findMethodExact(clazz, methodName, getParameterClasses(clazz.getClassLoader(), parameterTypesAndCallback));
return XposedBridge.hookMethod(m, callback);
}
首先,接口内部会根据类名和类加载器查找Class
对象,再根据Class
对象和方法名去查找Method
对象,最后调用XposedBridge.hookMethod
完成对方法的Hook
。
看一下方法查找的实现:
public static Method findMethodExact(String className, ClassLoader classLoader, String methodName, Object... parameterTypes) {
return findMethodExact(findClass(className, classLoader), methodName, getParameterClasses(classLoader, parameterTypes));
}
public static Method findMethodExact(Class> clazz, String methodName, Class>... parameterTypes) {
String fullMethodName = clazz.getName() + '#' + methodName + getParametersString(parameterTypes) + "#exact";
if (methodCache.containsKey(fullMethodName)) {
Method method = methodCache.get(fullMethodName);
if (method == null)
throw new NoSuchMethodError(fullMethodName);
return method;
}
try {
Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
method.setAccessible(true);
methodCache.put(fullMethodName, method);
return method;
} catch (NoSuchMethodException e) {
methodCache.put(fullMethodName, null);
throw new NoSuchMethodError(fullMethodName);
}
}
在findMethodExact
内部,fullMethodName
变量就是这个方法的完全表示名,形如java.net.URLEncoder#encode(java.lang.String,java.lang.String)#exact
。使用方法的完全表示名在methodCache表中查询,如果这个
Method对象在表中就从表中获取并返回,如果不在就创建一个新的
Method“`对象放入表中并返回。
进一步看一下XposedBridge.hookMethod
方法的实现,代码片段如下:
public static XC_MethodHook.Unhook hookMethod(Member hookMethod, XC_MethodHook callback) {
...
CopyOnWriteSortedSet callbacks;
...
callbacks.add(callback);
if (newMethod) {
Class> declaringClass = hookMethod.getDeclaringClass();
...
AdditionalHookInfo additionalInfo = new AdditionalHookInfo(callbacks, parameterTypes, returnType);
hookMethodNative(hookMethod, declaringClass, slot, additionalInfo);
}
return callback.new Unhook(hookMethod);
}
private native synchronized static void hookMethodNative(Member method, Class> declaringClass, int slot, Object additionalInfo);
这里的逻辑比较清晰,如果这个方法不存在对应的回调对象集合,那么就创建一个并把回调对象放入这个集合中。然后如果是新Hook
的方法,获取这个方法的参数和返回类型等信息作为参数去调用hookMethodNative
方法,这是一个native
方法,它注册的地方在libXposed_common.cpp
中,注册逻辑如下:
#define NATIVE_METHOD(className, functionName, signature) \
{ #functionName, signature, reinterpret_cast(className ## _ ## functionName) }
int register_natives_XposedBridge(JNIEnv* env, jclass clazz) {
const JNINativeMethod methods[] = {
...
NATIVE_METHOD(XposedBridge, hookMethodNative, "(Ljava/lang/reflect/Member;Ljava/lang/Class;ILjava/lang/Object;)V"),
...
};
return env->RegisterNatives(clazz, methods, NELEM(methods));
}
NATIVE_METHOD
是一个宏定义,根据这个宏定义,进一步找到hookMethodNative
对应的native
方法是XposedBridge_hookMethodNative
。这里以art
为例,这个方法的实现在libXposed_art.cpp
文件中,代码如下:
void XposedBridge_hookMethodNative(JNIEnv* env, jclass, jobject javaReflectedMethod,
jobject, jint, jobject javaAdditionalInfo) {
...
// Get the ArtMethod of the method to be hooked.
ScopedObjectAccess soa(env);
ArtMethod* artMethod = ArtMethod::FromReflectedMethod(soa, javaReflectedMethod);
// Hook the method
artMethod->EnableXposedHook(soa, javaAdditionalInfo);
}
通过FromReflectedMethod
获取要Hook
方法对应的ArtMethod
对象,然后再调用EnableXposedHook
接口。在art
虚拟机中,每一个加载的类方法都有一个对应的ArtMethod
对象,它的实现在ArtMethod.cc
中,下面是EnableXposedHook
方法的代码实现:
void ArtMethod::EnableXposedHook(ScopedObjectAccess& soa, jobject additional_info) {
...
// Create a backup of the ArtMethod object
auto* cl = Runtime::Current()->GetClassLinker();
ArtMethod* backup_method = cl->AllocArtMethodArray(soa.Self(), 1);
backup_method->CopyFrom(this, cl->GetImagePointerSize());
backup_method->SetAccessFlags(backup_method->GetAccessFlags() | kAccXposedOriginalMethod);
// Create a Method/Constructor object for the backup ArtMethod object
mirror::AbstractMethod* reflect_method;
if (IsConstructor()) {
reflect_method = mirror::Constructor::CreateFromArtMethod(soa.Self(), backup_method);
} else {
reflect_method = mirror::Method::CreateFromArtMethod(soa.Self(), backup_method);
}
reflect_method->SetAccessible<false>(true);
// Save extra information in a separate structure, stored instead of the native method
XposedHookInfo* hookInfo = reinterpret_cast<XposedHookInfo*>(calloc(1, sizeof(XposedHookInfo)));
hookInfo->reflectedMethod = soa.Vm()->AddGlobalRef(soa.Self(), reflect_method);
hookInfo->additionalInfo = soa.Env()->NewGlobalRef(additional_info);
hookInfo->originalMethod = backup_method;
SetEntryPointFromJni(reinterpret_cast<uint8_t*>(hookInfo));
ThreadList* tl = Runtime::Current()->GetThreadList();
soa.Self()->TransitionFromRunnableToSuspended(kSuspended);
tl->SuspendAll("Hooking method");
{
MutexLock mu(soa.Self(), *Locks::thread_list_lock_);
tl->ForEach(StackReplaceMethod, this);
}
tl->ResumeAll();
soa.Self()->TransitionFromSuspendedToRunnable();
SetEntryPointFromQuickCompiledCode(GetQuickProxyInvokeHandler());
SetEntryPointFromInterpreter(artInterpreterToCompiledCodeBridge);
// Adjust access flags
SetAccessFlags((GetAccessFlags() & ~kAccNative & ~kAccSynchronized) | kAccXposedHookedMethod);
}
这部分代码是关键所在,首先创建一个备份的ArtMethod
,并添加访问标志位kAccXposedOriginalMethod
,表示其为Hook
方法的原方法,然后为备份的ArtMethod
创建对应的Method
对象。把Method
对象、方法额外信息和原始方法保存至XposedHookInfo
结构体中,并调用SetEntryPointFromJni()
把这个结构体变量的内存地址保存在ArtMethod
对象中。这个位置原本是用来保存native
方法的入口地址的,既然使用了这个位置,那么就必须把对应的标志位清除,代码实现的最后调用SetAccessFlags((GetAccessFlags() & ~kAccNative & ~kAccSynchronized) | kAccXposedHookedMethod)
来完成标志位的清除。这并不会有问题,此时这个ArtMethod
对象对应是Hook
后的方法,这个方法的实现不是native
的。
接着看下面的代码逻辑,需要结合StackReplaceMethod()
的实现来分析:
static void StackReplaceMethod(Thread* thread, void* arg) SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) {
struct StackReplaceMethodVisitor FINAL : public StackVisitor {
StackReplaceMethodVisitor(Thread* thread_in, ArtMethod* search, ArtMethod* replace)
: StackVisitor(thread_in, nullptr, StackVisitor::StackWalkKind::kSkipInlinedFrames),
search_(search), replace_(replace) {};
bool VisitFrame() SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) {
if (GetMethod() == search_) {
SetMethod(replace_);
}
return true;
}
ArtMethod* search_;
ArtMethod* replace_;
};
ArtMethod* search = reinterpret_cast(arg);
// We cannot use GetXposedOriginalMethod() because the access flags aren't modified yet.
auto hook_info = reinterpret_cast<const XposedHookInfo*>(search->GetEntryPointFromJni());
ArtMethod* replace = hook_info->originalMethod;
StackReplaceMethodVisitor visitor(thread, search, replace);
visitor.WalkStack();
}
它实现的功能是挂起所有线程并在每一个线程中查找是否存在这个修改后的ArtMethod
对象,有就替换成未修改前的ArtMethod
对象。
再回到EnableXposedHook
中,接着调用 SetEntryPointFromQuickCompiledCode(GetQuickProxyInvokeHandler())
和SetEntryPointFromInterpreter(artInterpreterToCompiledCodeBridge)分别设置机器指令和解释器的入口地址。从解释器入口进入之后会调用artInterpreterToCompiledCodeBridge
,下面是artInterpreterToCompiledCodeBridge
代码片段:
extern "C" void artInterpreterToCompiledCodeBridge(Thread* self, const DexFile::CodeItem* code_item,
ShadowFrame* shadow_frame, JValue* result) {
...
method->Invoke(self, shadow_frame->GetVRegArgs(arg_offset),
(shadow_frame->NumberOfVRegs() - arg_offset) * sizeof(uint32_t),
result, method->GetInterfaceMethodIfProxy(sizeof(void*))->GetShorty());
}
前面的代码逻辑都先不用关注,看最后一行调用了ArtMehod
的Invoke
方法,这正是机器指令执行的路径。所以说,当被Hook
的方法被调用时,不管是机器指令还是解释器执行的,都会进入ArtMethod::Invoke()
中,下面是它的代码片段:
void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* result, const char* shorty) {
...
if (LIKELY(have_quick_code)) {
if (!IsStatic()) {
(*art_quick_invoke_stub)(this, args, args_size, self, result, shorty);
} else {
(*art_quick_invoke_static_stub)(this, args, args_size, self, result, shorty);
}
}
...
}
ArtMethod::Invoke()
会进一步调用art_quick_invoke_stub
,art_quick_invoke_stub
的内部实现是汇编语言的,art_quick_invoke_stub
与具体的机器架构相关,这里以arm
架构为例:
ENTRY art_quick_invoke_stub
push {r0, r4, r5, r9, r11, lr} @ spill regs
.save {r0, r4, r5, r9, r11, lr}
.pad #24
.cfi_adjust_cfa_offset 24
.cfi_rel_offset r0, 0
.cfi_rel_offset r4, 4
.cfi_rel_offset r5, 8
.cfi_rel_offset r9, 12
.cfi_rel_offset r11, 16
.cfi_rel_offset lr, 20
mov r11, sp @ save the stack pointer
.cfi_def_cfa_register r11
mov r9, r3 @ move managed thread pointer into r9
#ifdef ARM_R4_SUSPEND_FLAG
mov r4, #SUSPEND_CHECK_INTERVAL @ reset r4 to suspend check interval
#endif
add r5, r2, #4 @ create space for method pointer in frame
sub r5, sp, r5 @ reserve & align *stack* to 16 bytes: native calling
and r5, #0xFFFFFFF0 @ convention only aligns to 8B, so we have to ensure ART
mov sp, r5 @ 16B alignment ourselves.
add r0, sp, #4 @ pass stack pointer + method ptr as dest for memcpy
bl memcpy @ memcpy (dest, src, bytes)
ldr r0, [r11] @ restore method*
ldr r1, [sp, #4] @ copy arg value for r1
ldr r2, [sp, #8] @ copy arg value for r2
ldr r3, [sp, #12] @ copy arg value for r3
mov ip, #0 @ set ip to 0
str ip, [sp] @ store NULL for method* at bottom of frame
ldr ip, [r0, #METHOD_QUICK_CODE_OFFSET_32] @ get pointer to the code
blx ip @ call the method
mov sp, r11 @ restore the stack pointer
ldr ip, [sp, #24] @ load the result pointer
strd r0, [ip] @ store r0/r1 into result pointer
pop {r0, r4, r5, r9, r11, lr} @ restore spill regs
.cfi_restore r0
.cfi_restore r4
.cfi_restore r5
.cfi_restore r9
.cfi_restore lr
.cfi_adjust_cfa_offset -24
bx lr
END art_quick_invoke_stub
汇编指令blx ip
跳转到entry_point_from_compiled_code_
指定的地址,这个地址就是通过SetEntryPointFromQuickCompiledCode()
函数设置的,可知此时执行流跳入到GetQuickProxyInvokeHandler()
的地址中,即artQuickProxyInvokeHandler
:
runtime_asm_entrypoints.h
extern "C" void art_quick_proxy_invoke_handler();
static inline const void* GetQuickProxyInvokeHandler() {
return reinterpret_cast(art_quick_proxy_invoke_handler);
}
quick_trampoline_entrypoint.cc
extern "C" uint64_t artQuickProxyInvokeHandler(
ArtMethod* proxy_method, mirror::Object* receiver, Thread* self, ArtMethod** sp) {
...
if (is_xposed) {
jmethodID proxy_methodid = soa.EncodeMethod(proxy_method);
self->EndAssertNoThreadSuspension(old_cause);
JValue result = InvokeXposedHandleHookedMethod(soa, shorty, rcvr_jobj, proxy_methodid, args);
local_ref_visitor.FixupReferences();
return result.GetJ();
}
...
}
这个代理方法会判断当前是否是xposed
环境,如果是就调用InvokeXposedHandleHookedMethod()
方法。
JValue InvokeXposedHandleHookedMethod(ScopedObjectAccessAlreadyRunnable& soa, const char* shorty, jobject rcvr_jobj, jmethodID method, std::vector& args) {
...
const XposedHookInfo* hookInfo = soa.DecodeMethod(method)->GetXposedHookInfo();
// Call XposedBridge.handleHookedMethod(Member method, int originalMethodId, Object additionalInfoObj,
// Object thisObject, Object[] args)
jvalue invocation_args[5];
invocation_args[0].l = hookInfo->reflectedMethod;
invocation_args[1].i = 1;
invocation_args[2].l = hookInfo->additionalInfo;
invocation_args[3].l = rcvr_jobj;
invocation_args[4].l = args_jobj;
jobject result =
soa.Env()->CallStaticObjectMethodA(ArtMethod::xposed_callback_class,
ArtMethod::xposed_callback_method,
invocation_args);
...
}
这个方法的内部实现是,先获取保存在ArtMethod
对象中的XposedHookInfo
数据,然后通过CallStaticObjectMethodA
接口调用XposedBridge.handleHookedMethod()
方法,注意此时已经从native
中跳入到JAVA
里了。继续看XposedBridge.handleHookedMethod()
方法的实现:
private static Object handleHookedMethod(Member method, int originalMethodId, Object additionalInfoObj,
Object thisObject, Object[] args) throws Throwable {
...
MethodHookParam param = new MethodHookParam();
param.method = method;
param.thisObject = thisObject;
param.args = args;
// call "before method" callbacks
int beforeIdx = 0;
do {
try {
((XC_MethodHook) callbacksSnapshot[beforeIdx]).beforeHookedMethod(param);
} catch (Throwable t) {
XposedBridge.log(t);
// reset result (ignoring what the unexpectedly exiting callback did)
param.setResult(null);
param.returnEarly = false;
continue;
}
if (param.returnEarly) {
// skip remaining "before" callbacks and corresponding "after" callbacks
beforeIdx++;
break;
}
} while (++beforeIdx < callbacksLength);
// call original method if not requested otherwise
if (!param.returnEarly) {
try {
param.setResult(invokeOriginalMethodNative(method, originalMethodId,
additionalInfo.parameterTypes, additionalInfo.returnType, param.thisObject, param.args));
} catch (InvocationTargetException e) {
param.setThrowable(e.getCause());
}
}
// call "after method" callbacks
int afterIdx = beforeIdx - 1;
do {
Object lastResult = param.getResult();
Throwable lastThrowable = param.getThrowable();
try {
((XC_MethodHook) callbacksSnapshot[afterIdx]).afterHookedMethod(param);
} catch (Throwable t) {
XposedBridge.log(t);
// reset to last result (ignoring what the unexpectedly exiting callback did)
if (lastThrowable == null)
param.setResult(lastResult);
else
param.setThrowable(lastThrowable);
}
} while (--afterIdx >= 0);
// return
if (param.hasThrowable())
throw param.getThrowable();
else
return param.getResult();
}
进入handleHookedMethod()
后,首先调用所有before method
的回调方法,然后调用原始方法,最后调用所有after method
的回调方法。到这里就可以看出跟之前应用层的代码形成的对应关系。
Xposed framework
通过invokeOriginalMethodNative
方法去调用原方法,其对应的native
方法如下:
jobject XposedBridge_invokeOriginalMethodNative(JNIEnv* env, jclass, jobject javaMethod,
jint isResolved, jobjectArray, jclass, jobject javaReceiver, jobjectArray javaArgs) {
ScopedFastNativeObjectAccess soa(env);
if (UNLIKELY(!isResolved)) {
ArtMethod* artMethod = ArtMethod::FromReflectedMethod(soa, javaMethod);
if (LIKELY(artMethod->IsXposedHookedMethod())) {
javaMethod = artMethod->GetXposedHookInfo()->reflectedMethod;
}
}
#if PLATFORM_SDK_VERSION >= 23
return InvokeMethod(soa, javaMethod, javaReceiver, javaArgs);
#else
return InvokeMethod(soa, javaMethod, javaReceiver, javaArgs, true);
#endif
}
InvokeMethod()
的实现在Reflection.cc
中,代码如下所示:
jobject InvokeMethod(const ScopedObjectAccessAlreadyRunnable& soa, jobject javaMethod,
...
mirror::ArtMethod* m = mirror::ArtMethod::FromReflectedMethod(soa, javaMethod);
...
// Invoke the method.
JValue result;
uint32_t shorty_len = 0;
const char* shorty = m->GetShorty(&shorty_len);
ArgArray arg_array(shorty, shorty_len);
StackHandleScope<1> hs(soa.Self());
MethodHelper mh(hs.NewHandle(m));
if (!arg_array.BuildArgArrayFromObjectArray(soa, receiver, objects, mh)) {
CHECK(soa.Self()->IsExceptionPending());
return nullptr;
}
InvokeWithArgArray(soa, m, &arg_array, &result, shorty);
通过FromReflectedMethod()
获取到Method
对象对应的ArtMethod
对象,这个就是在Hook
时创建的原始ArtMethod
对象的备份。然后,再通过InvokeWithArgArray
调用原始的方法。
至此,完成了对Xposed framework Hook
的源码分析。我们再整体看一下它的流程,当Hook
一个方法时,首先需要提供方法的所在的包名、方法名、参数类型以及回调对象去调用Hook
接口;Xposed
在art
虚拟机中找到方法对应的ArtMethod
对象,备份这个ArtMethod
对象,然后修改这个对象的解释器和机器指令入口,关键所在。然后把每个线程中加载的这个被修改后的ArtMethod
对象替换成原始备份的ArtMethod
对象。最后在修改的机器指令入口地址指向的函数里,实现回调方法和原始方法的调用。
很多细节还没有涉及,下面会从/system/bin/app_process
的实现着手分析其框架部分代码的实现。