最近对art虚拟机比较感兴趣,因此就选了ARTHook作为切入点(深入)理解下。选了比较有名的epic
,本身该框架考虑的点比较完善,api也比较友好,挺适合学习的。
ARTMethod
Java 对象在内存中的布局可以看成一个结构体,父类的变量在开头,本身的变量紧随其后。
这些对象结构体在 ART 中被映射成 mirror::Object cpp 类
// C++ mirror of java.lang.reflect.Method.
class MANAGED Method : public Executable {
....
}
// C++ mirror of java.lang.reflect.Executable.
class MANAGED Executable : public AccessibleObject {
uint16_t has_real_parameter_data_;
HeapReference<mirror::Class> declaring_class_;
HeapReference<mirror::Class> declaring_class_of_overridden_method_;
HeapReference<mirror::Array> parameters_;
// ArtMethod 地址
uint64_t art_method_;
uint32_t access_flags_;
uint32_t dex_method_index_;
}
对于java.lang.reflect.Method
(java对象),其父类在AndroidO以上为java.lang.reflect.Executable
。通过获取Executable
中的artMethod
变量,可以拿到ArtMethod
LinkCode
看老罗的art类加载就知道了,art中分解释执行与本地机器指令执行。所以就会存在以下几种情况:
- 解释执行函数调用本地执行函数
- 本地执行函数调用解释执行函数
- 解释进入解释
- 本地进入本地
所以就衍生除了很多enteyCode
static void LinkCode(ClassLinker* class_linker,
ArtMethod* method,
const OatFile::OatClass* oat_class,
uint32_t class_def_method_index) REQUIRES_SHARED(Locks::mutator_lock_) {
if (oat_class != nullptr) {
// 判断方法是否已经被 OAT
const OatFile::OatMethod oat_method = oat_class->GetOatMethod(class_def_method_index);
oat_method.LinkMethod(method);
}
// Install entry point from interpreter.
const void* quick_code = method->GetEntryPointFromQuickCompiledCode();
bool enter_interpreter = class_linker->ShouldUseInterpreterEntrypoint(method, quick_code);
//如果是从解释器进来的
if (enter_interpreter) { //如果要进入解释执行,那就是解释进入解释
method->SetEntryPointFromInterpreter(interpreter::artInterpreterToInterpreterBridge);
} else { //解释进入本地
method->SetEntryPointFromInterpreter(artInterpreterToCompiledCodeBridge);
}
//如果是从本地指令进来的
if (method->IsStatic() && !method->IsConstructor()) {
// 对于静态方法,后面会在 ClassLinker::InitializeClass 里被 ClassLinker::FixupStaticTrampolines 替换掉,先设置为stub,后面要考
method->SetEntryPointFromQuickCompiledCode(GetQuickResolutionStub());
} else if (quick_code == nullptr && method->IsNative()) {
// Native 方法跳转到 JNI
method->SetEntryPointFromQuickCompiledCode(GetQuickGenericJniStub());
} else if (enter_interpreter) {
// 解释模式,跳转到解释器
method->SetEntryPointFromQuickCompiledCode(GetQuickToInterpreterBridge());
}
// ...
}
Q: 如何判断thumb指令还是art指令
在ARM-v7A中常使用32位ARM指令集并且支持thumb指令集与arm的切换,而在ARMV8中使用的是64位ARM指令集且不再有thumb指令集状态的切换了.
目前,ARM是三级流水线,因此,当CPU在执行S指令的时候,PC指向的是S+2指令。但是当手动向PC赋值,则是让CPU跳转到赋入的值 所代表的地址去运行。
注:通常PC指针指向的地址都是4字节对齐,即地址的[1:0]位总是为0,这也是我们说的ARM模式。现在很多CPU都支持混合编码即同时支持ARM指令和Thumb指令,因此为了区分Thumb指令,ARM将[0]位设置成1,即地址最低位如果是1,表示当前指令是Thumb指令,否则为ARM指令。Thumb模式到ARM模式可以通过带X的跳转进行切换
区分可使用:
isThumb = ((entryPointFromQuickCompiledCode & 1) == 1);
Q: BL/BLX等跳转指令区别
Q: JNI中的 jobject,Java中的Object,ART 中的 art::mirror::Object 到底是个什么关系
art::mirror::Object 是 Java的Object在Runtime中的表示,java.lang.Object的地址就是art::mirror::Object的地址;但是jobject略有不同,它并非地址,而是一个句柄(或者说透明引用)。为何如此?
因为JNI对于ART来说是外部环境,如果直接把ART中的对象地址交给JNI层(也就是jobject直接就是Object的地址),其一不是很安全,其二直接暴露内部实现不妥。就拿GC来说,虚拟机在GC过程中很可能移动对象,这样对象的地址就会发生变化,如果JNI直接使用地址,那么对GC的实现提出了很高要求。因此,典型的Java虚拟机对JNI的支持中,jobject都是句柄(或者称之为透明引用);ART虚拟机内部可以在joject与 art::mirror::Object中自由转换,但是JNI层只能拿这个句柄去标志某个对象。
Q: ART函数的调用约定
Thumb2为例,子函数调用的参数传递是通过寄存器r0~r3 以及sp寄存器完成的。r0 ~ r3 依次传递第一个至第4个参数,同时 sp, (sp + 4), (sp + 8), (sp + 12) 也存放着r0~r3上对应的值;
在ART中,r0寄存器固定存放被调用方法的ArtMethod指针,如果是non-static 方法,r1寄存器存放方法的this对象;
Q: dex_cache是什么,什么时候引入
dex_cache_resolved_methods_是一个指针数组,保存的是ArtMethod结构指针。。顾名思义,这个数组用于缓存解析的方法。通过它可以获得ArtMethod所在dex所有Method对应的ArtMethod*。
Q: 什么是unSafe
Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力
如下就是怎么object&address互转
public static long getObjectAddress(Object obj) {
try {
Object[] array = new Object[]{obj};
if (arrayIndexScale(Object[].class) == 8) {
return getLong(array, arrayBaseOffset(Object[].class));
} else {
return 0xffffffffL & getInt(array, arrayBaseOffset(Object[].class));
}
} catch (Exception e) {
Log.w(TAG, e);
return -1;
}
}
/**
* get Object from address, refer: http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/
* @param address the address of a object.
* @return
*/
public static Object getObject(long address) {
Object[] array = new Object[]{null};
long baseOffset = arrayBaseOffset(Object[].class);
if (Runtime.is64Bit()) {
putLong(array, baseOffset, address);
} else {
putInt(array, baseOffset, (int) address);
}
return array[0];
}
备份
对于要hook的函数,首先对其进行备份:
看下面的代码得出:
Method
中有变量artMethod
:java.lang.reflect.Executable.artMethod
,所以就创建一个新的Method
,拷贝其中的artMethod即可。
Class<?> abstractMethodClass = Method.class.getSuperclass();
Object executable = this.getExecutable();
ArtMethod artMethod;
if (Build.VERSION.SDK_INT < 23) {
Class<?> artMethodClass = Class.forName("java.lang.reflect.ArtMethod");
//Get the original artMethod field, 拿到artMethod字段
Field artMethodField = abstractMethodClass.getDeclaredField("artMethod");
if (!artMethodField.isAccessible()) {
artMethodField.setAccessible(true);
}
Object srcArtMethod = artMethodField.get(executable);
//创建一个artmethod
Constructor<?> constructor = artMethodClass.getDeclaredConstructor();
constructor.setAccessible(true);
Object destArtMethod = constructor.newInstance();
//Fill the fields to the new method we created
for (Field field : artMethodClass.getDeclaredFields()) {
if (!field.isAccessible()) {
field.setAccessible(true);
}
field.set(destArtMethod, field.get(srcArtMethod));
}
Method newMethod = Method.class.getConstructor(artMethodClass).newInstance(destArtMethod);
newMethod.setAccessible(true);
artMethod = ArtMethod.of(newMethod);
artMethod.setEntryPointFromQuickCompiledCode(getEntryPointFromQuickCompiledCode());
artMethod.setEntryPointFromJni(getEntryPointFromJni());
} else {
//private java.lang.reflect.Method()
Constructor<Method> constructor = Method.class.getDeclaredConstructor();
// we can't use constructor.setAccessible(true); because Google does not like it
// AccessibleObject.setAccessible(new AccessibleObject[]{constructor}, true);
Field override = AccessibleObject.class.getDeclaredField(
Build.VERSION.SDK_INT == Build.VERSION_CODES.M ? "flag" : "override");
override.setAccessible(true);
override.set(constructor, true);
Method m = constructor.newInstance();
m.setAccessible(true);
for (Field field : abstractMethodClass.getDeclaredFields()) {
field.setAccessible(true);
field.set(m, field.get(executable));
}
Field artMethodField = abstractMethodClass.getDeclaredField("artMethod");
//private long java.lang.reflect.Executable.artMethod
artMethodField.setAccessible(true);
int artMethodSize = getArtMethodSize();
long memoryAddress = EpicNative.map(artMethodSize);
byte[] data = EpicNative.get(address, artMethodSize);
EpicNative.put(data, memoryAddress);
artMethodField.set(m, memoryAddress);
// From Android R, getting method address may involve the jni_id_manager which uses
// ids mapping instead of directly returning the method address. During resolving the
// id->address mapping, it will assume the art method to be from the "methods_" array
// in class. However this address may be out of the range of the methods array. Thus
// it will cause a crash during using the method offset to resolve method array.
artMethod = ArtMethod.of(m, memoryAddress);
}
artMethod.makePrivate();
artMethod.setAccessible(true);
artMethod.origin = this; // save origin method.
return artMethod;
basic
AbstractMethod类中对应的artMethod属性的值可以作为c层ArtMethod的地址直接使用,即我们在Java拿到的artMethod就是c层ArtMethod的实际地址
@@art/mirror/abstract_method.cc
ArtMethod* AbstractMethod::GetArtMethod() {
return reinterpret_cast<ArtMethod*>(GetField64(ArtMethodOffset()));
}
@@art/mirror/abstract_method.h
static MemberOffset ArtMethodOffset() {
return MemberOffset(OFFSETOF_MEMBER(AbstractMethod, art_method_));
}
Q 如何获取方法大小
ArtMethod 被存放在线性内存区域,并且不会 Moving GC,那么,相邻的两个方法他们的 ArtMethod 也是相邻的,所以 size = ArtMethod2 - ArtMethod1
public static int getArtMethodSize() {
if (artMethodSize > 0) {
return artMethodSize;
}
final Method rule1 = XposedHelpers.findMethodExact(ArtMethod.class, "rule1");
final Method rule2 = XposedHelpers.findMethodExact(ArtMethod.class, "rule2");
final long rule2Address = EpicNative.getMethodAddress(rule2);
final long rule1Address = EpicNative.getMethodAddress(rule1);
final long size = Math.abs(rule2Address - rule1Address);
artMethodSize = (int) size;
Logger.d(TAG, "art Method size: " + size);
return artMethodSize;
}
这里看到epic很讨巧,在一个自定义的ArtMethod中加了两个函数,然后算这两个函数的地址差。
jlong epic_getMethodAddress(JNIEnv *env, jclass clazz, jobject method) {
jlong art_method = (jlong) env->FromReflectedMethod(method);
这里获取地址用了FromReflectedMethod
,env->FromReflectedMethod(src) 返回的是 jmethodID ,事实上就是 ArtMethod 结构体的指针地址,所以可以强制类型转换成 ArtMethod 结构体指针
静态方法
art_quick_resolution_trampoline, 同理androidN以上默认不进行aot编译。
哪些不同的Java方法会具有相同的compiled_code入口点呢?
1、所有ART版本上未被resolve的static函数 art_quick_resolution_trampoline
2、Android N 以上的未被编译的所有函数
3、代码逻辑一模一样的函数
4、JNI函数
所以epic进行了强制编译:
artOrigin.ensureResolved();
public Object invoke(Object receiver, Object... args) throws IllegalAccessException, InvocationTargetException, InstantiationException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (origin != null) {
byte[] currentAddress = EpicNative.get(origin.address, 4);
byte[] backupAddress = EpicNative.get(address, 4);
if (!Arrays.equals(currentAddress, backupAddress)) {
if (Debug.DEBUG) {
Logger.i(TAG, "the address of java method was moved by gc, backup it now! origin address: 0x"
+ Arrays.toString(currentAddress) + " , currentAddress: 0x" + Arrays.toString(backupAddress));
}
EpicNative.put(currentAddress, address);
return invokeInternal(receiver, args);
} else {
Logger.i(TAG, "the address is same with last invoke, not moved by gc");
}
}
}
return invokeInternal(receiver, args);
}
//在N以下,如果是构造函数,触发一次instance,否则就执行一次method即可。
private Object invokeInternal(Object receiver, Object... args) throws IllegalAccessException, InvocationTargetException, InstantiationException {
if (constructor != null) {
return constructor.newInstance(args);
} else {
return method.invoke(receiver, args);
}
}
对于当前还是解释执行
则需要编译一次artOrigin.compile()
简单而言就通过jit_compile_method即可。jit_compile_method_ = (bool (*)(void *, void *, void *, bool)) dlsym_ex(jit_lib, "jit_compile_method");
jboolean epic_compile(JNIEnv *env, jclass, jobject method, jlong self) {
LOGV("self from native peer: %p, from register: %p", reinterpret_cast<void*>(self), __self());
jlong art_method = (jlong) env->FromReflectedMethod(method);
if (art_method % 2 == 1) {
art_method = reinterpret_cast<jlong>(JniIdManager_DecodeMethodId_(ArtHelper::getJniIdManager(), art_method));
}
bool ret;
if (api_level >= 29) {
ret = ((JIT_COMPILE_METHOD2) jit_compile_method_)(jit_compiler_handle_,
reinterpret_cast<void *>(art_method),
reinterpret_cast<void *>(self), false, false);
} else {
ret = ((JIT_COMPILE_METHOD1) jit_compile_method_)(jit_compiler_handle_,
reinterpret_cast<void *>(art_method),
reinterpret_cast<void *>(self), false);
}
return (jboolean)ret;
}
下面还是进入正题开始跳转
Trampoline
epic的精髓就是callee跳转,即被调用方跳转。为啥要设置两段跳板呢,因为二端跳板其实很长,在原有的artMethod的compileCode指向的方法代码中可能放不下
1. 创建二段跳板
(这里不说怎么看thumb指令了自己学)
r0指的是当前artMethod的地址,如果与source method地址不相同,则直接执行原方法。
其实很简单,先看当前的r0和你想hook的method是不是同一个,
然后把sp, r2,r3, source_method_address放到ip指向的struct,然后跳到target_method_entry_point执行。
byte[] instructions = new byte[]{
(byte) 0xdf, (byte) 0xf8, (byte) 0x30, (byte) 0xc0, // ldr ip, [pc, #48] ip = source method address
(byte) 0x60, (byte) 0x45, // cmp r0, ip if r0 != ip
(byte) 0x40, (byte) 0xf0, (byte) 0x19, (byte) 0x80, // bne.w 1f jump label 1:
(byte) 0x08, (byte) 0x48, // ldr r0, [pc, #28] r0 = target_method_address
(byte) 0xdf, (byte) 0xf8, (byte) 0x28, (byte) 0xc0, // ldr ip, [pc, #38] ip = struct address
(byte) 0xcc, (byte) 0xf8, (byte) 0x00, (byte) 0xd0, // str sp, [ip, #0]
(byte) 0xcc, (byte) 0xf8, (byte) 0x04, (byte) 0x20, // str r2, [ip, #4]
(byte) 0xcc, (byte) 0xf8, (byte) 0x08, (byte) 0x30, // str r3, [ip, #8]
(byte) 0x63, (byte) 0x46, // mov r3, ip
(byte) 0x05, (byte) 0x4a, // ldr r2, [pc, #16] r2 = source_method_address
(byte) 0xcc, (byte) 0xf8, (byte) 0x0c, (byte) 0x20, // str r2, [ip, #12]
(byte) 0x4a, (byte) 0x46, // move r2, r9
(byte) 0x4a, (byte) 0x46, // move r2, r9
(byte) 0xdf, (byte) 0xf8, (byte) 0x04, (byte) 0xf0, // ldr pc, [pc, #4]
0x0, 0x0, 0x0, 0x0, // target_method_pos_x
0x0, 0x0, 0x0, 0x0, // target_method_entry_point
0x0, 0x0, 0x0, 0x0, // src_method_address
0x0, 0x0, 0x0, 0x0, // struct address (sp, r1, r2)
// 1:
};
所以这段就是构造参数然后跳到图上的java bridge执行。(bridge是什么后面说)
有关堆栈平衡
如果我们在二段跳板代码里面开辟堆栈,进而修改了sp寄存器;那么在我们修改sp到调用bridge函数的这段时间里,堆栈结构与不Hook的时候是不一样的(虽然bridge函数执行完毕之后我们可以恢复正常);在这段时间里如果虚拟机需要进行栈回溯,sp被修改的那一帧会由于回溯不到对应的函数引发致命错误,导致Runtime 直接Abort。
对于堆栈平衡而言,最重要即为esp不能修改。
Trampoline 一段跳转指令
执行方法时不执行quickCompileCode地址,跳到target pc执行。即跳到二段代码。
private boolean activate() {
long pc = getTrampolinePc();
Logger.d(TAG, "Writing direct jump entry " + Debug.addrHex(pc) + " to origin entry: 0x" + Debug.addrHex(jumpToAddress));
synchronized (Trampoline.class) {
return EpicNative.activateNative(jumpToAddress, pc, shellCode.sizeOfDirectJump(),
shellCode.sizeOfBridgeJump(), shellCode.createDirectJump(pc));
}
}
正常来说这里跟二段跳板一下改一下内存就行,但这里跳到了native:
我们先看传入的参数:
- jumpToAddress 原方法的entryCode
- pc 二段跳板的地址
- sizeOfDirectJump
- sizeOfBridgeJump
- createDirectJump(pc) 构造跳到二段跳板的方法
整个方法即把sourceMethod.entryCode的前面几个字节改成跳二段跳板的地址。【跳】二段跳板=一段跳板
createDirectJump
@Override
public byte[] createDirectJump(long targetAddress) {
byte[] instructions = new byte[] {
(byte) 0xdf, (byte) 0xf8, 0x00, (byte) 0xf0, // ldr pc, [pc]
0, 0, 0, 0
};
writeInt((int) targetAddress, ByteOrder.LITTLE_ENDIAN, instructions,
instructions.length - 4);
return instructions;
}
看代码和分析,从androidN开始因为引入了混合编译,所以随时jit线程都有可能更改code。为了原子操作所以需要暂停所有线程操作。
jboolean epic_activate(JNIEnv* env, jclass jclazz, jlong jumpToAddress, jlong pc, jlong sizeOfDirectJump,
jlong sizeOfBridgeJump, jbyteArray code) {
// fetch the array, we can not call this when thread suspend(may lead deadlock)
jbyte *srcPnt = env->GetByteArrayElements(code, 0);
jsize length = env->GetArrayLength(code);
jlong cookie = 0;
bool isNougat = api_level >= 24;
if (isNougat) {
// We do thus things:
// 1. modify the code mprotect
// 2. modify the code
// Ideal, this two operation must be atomic. Below N, this is safe, because no one
// modify the code except ourselves;
// But in Android N, When the jit is working, between our step 1 and step 2,
// if we modity the mprotect of the code, and planning to write the code,
// the jit thread may modify the mprotect of the code meanwhile
// we must suspend all thread to ensure the atomic operation.
LOGV("suspend all thread.");
cookie = epic_suspendAll(env, jclazz);
}
jboolean result = epic_munprotect(env, jclazz, jumpToAddress, sizeOfDirectJump);
if (result) {
unsigned char *destPnt = (unsigned char *) jumpToAddress;
for (int i = 0; i < length; ++i) {
destPnt[i] = (unsigned char) srcPnt[i];
}
jboolean ret = epic_cacheflush(env, jclazz, pc, sizeOfBridgeJump);
if (!ret) {
LOGV("cache flush failed!!");
}
} else {
LOGV("Writing hook failed: Unable to unprotect memory at %d", jumpToAddress);
}
if (cookie != 0) {
LOGV("resume all thread.");
epic_resumeAll(env, jclazz, cookie);
}
env->ReleaseByteArrayElements(code, srcPnt, 0);
return result;
}
origin
除了Trampoline代码外,还有一段老逻辑,这也是前面jump label 1:可以直接跳到方法最后的原因,因为原方法就拼在后面。
for (ArtMethod method : segments) {
byte[] bridgeJump = createTrampoline(method);
int length = bridgeJump.length;
System.arraycopy(bridgeJump, 0, mainPage, offset, length);
offset += length;
}
byte[] callOriginal = shellCode.createCallOrigin(jumpToAddress, originalCode);
System.arraycopy(callOriginal, 0, mainPage, offset, callOriginal.length);
origin为2*directJump指令的大小。
第一段为原originalPrologue(即原quickCompileCode取directJump大小的指令)
第二段为createDirectJump:
byte[] instructions = new byte[] {
(byte) 0xdf, (byte) 0xf8, 0x00, (byte) 0xf0, // ldr pc, [pc]
0, 0, 0, 0
};
public byte[] createCallOrigin(long originalAddress, byte[] originalPrologue) {
byte[] callOriginal = new byte[sizeOfCallOrigin()];
System.arraycopy(originalPrologue, 0, callOriginal, 0, sizeOfDirectJump()); //(Object src, int srcPos, Object dest, int destPos, int length)
byte[] directJump = createDirectJump(toPC(originalAddress + sizeOfDirectJump()));
System.arraycopy(directJump, 0, callOriginal, sizeOfDirectJump(), directJump.length);
return callOriginal;
}
排布即为:
//先原ArtMethod从compileCode开始的几个字节
//directJump代码,跳到一段代码后面的代码
可以理解为将原先的compileCode分成A+B A变成一段代码,即跳到B。
一段跳转代码都放不下
if (quickCompiledCodeSize < sizeOfDirectJump) {
Logger.w(TAG, originMethod.toGenericString() + " quickCompiledCodeSize: " + quickCompiledCodeSize);
originMethod.setEntryPointFromQuickCompiledCode(getTrampolinePc());
return true;
}
其他知识
entrypoint replacement
但定义在boot.oat里的代码都已经是用绝对地址访问:
boot.oat里面如果要使用某个类、field、method,只要它在boot.art中被定义,那么就可以直接使用决定地址来访问。因为boot.oat 这个文件在内存中的加载地址是固定的
Android7.0
开始混合编译,Android N采用了混合编译的模式,既有解释执行,也有AOT和JIT;APK刚安装完毕是解释执行的,运行时JIT会收集方法调用信息,必要的时候直接编译此方法,甚至栈上替换
Android8.0
跨dex方法内联
Android 9.0
私有api调用限制 使用自己解析elf文件使用dlopen&dlsym访问
todo
http://rk700.github.io/2017/03/30/YAHFA-introduction/