android java層實現hook替換method

  Android上的熱修復框架 AndFix 你們都很熟悉了,它的原理實際上很簡單:html

  方法替換——Java層的每個方法在虛擬機實現裏面都對應着一個ArtMethod的結構體,只要把原方法的結構體內容替換成新的結構體的內容,在調用原方法的時候,真正執行的指令會是新方法的指令;這樣就能實現熱修復,詳細代碼見 AndFix。須要瞭解Android 虛擬機的方法調用過程才能完全理解。java

衆所周知,AndFix是一種 native 的hotfix方案,它的替換過程是用 c 在 native層完成的,但其實,咱們也能夠用純Java實現它!android

方法替換原理

既然咱們知道 AndFix 的原理是方法替換,那麼爲何直接替換Java裏面的 java.lang.reflect.Method 有什麼問題嗎?直接這樣貌似很難下結論,那咱們換個思路。咱們實現方法替換的結果,就是調用原方法的時候最終是調用被替換的方法。所以,咱們能夠看看 java.lang.reflect.Method類的 invoke 方法。(Foo.bar()這種直接調用與反射調用Foo.class.getDeclaredMethod(「bar」).invoke(null) 有什麼區別嗎?)c++

1 private native Object invoke(Object receiver, Object[] args, boolean accessible)
2         throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;

  這個invoke是一個native方法,它的native實如今 art/runtime/native/java_lang_reflect_Method.cc 裏面,這個jni方法最終調用了 art/runtime/reflection.cc 的 InvokeMethod方法:git

 1 object InvokeMethod(const ScopedObjectAccessAlreadyRunnable& soa, jobject javaMethod,
 2                      jobject javaReceiver, jobject javaArgs, bool accessible) {
 3   // 略...
 4 
 5   mirror::ArtMethod* m = mirror::ArtMethod::FromReflectedMethod(soa, javaMethod);
 6 
 7   mirror::Class* declaring_class = m->GetDeclaringClass();
 8 
 9   // 按需初始化類,略。。
10 
11   mirror::Object* receiver = nullptr;
12   if (!m->IsStatic()) {
13     // Check that the receiver is non-null and an instance of the field's declaring class.
14     receiver = soa.Decode<mirror::Object*>(javaReceiver);
15     if (!VerifyObjectIsClass(receiver, declaring_class)) {
16       return NULL;
17     }
18 
19     // Find the actual implementation of the virtual method.
20     m = receiver->GetClass()->FindVirtualMethodForVirtualOrInterface(m);
21   }
22 
23   // 略..
24   InvokeWithArgArray(soa, m, &arg_array, &result, shorty);
25   // 略 。。
26   // Box if necessary and return.
27   return soa.AddLocalReference<jobject>(BoxPrimitive(mh.GetReturnType()->GetPrimitiveType(),
28                                                      result));
29 }

 

上面函數 InvokeMethod 的第二個參數 javaMethod 就是Java層咱們進行反射調用的那個Method對象,在jni層反映爲一個jobject;InvokeMethod這個native方法首先經過 mirror::ArtMethod::FromReflectedMethod 獲取了Java對象的在native層的 ArtMethod指針,咱們跟進去看看是怎麼實現的:github

1 ArtMethod* ArtMethod::FromReflectedMethod(const ScopedObjectAccessAlreadyRunnable& soa,  jobject jlr_method) {
2
3 mirror::ArtField* f = 4 soa.DecodeField(WellKnownClasses::java_lang_reflect_AbstractMethod_artMethod); 5 mirror::ArtMethod* method = f->GetObject(soa.Decode<mirror::Object*>(jlr_method))->AsArtMethod(); 6 DCHECK(method != nullptr); 7 return method; 8 }

AndFix的實現裏面,也正是使用這個 FromReflectedMethod 方法拿到Java層Method對應native層的ArtMethod指針,而後執行替換的。咱們在這裏看到了一點端倪,獲取到了Java層那個Method對象的一個叫作 artMethod的字段,而後強轉成了ArtMethod指針(這裏的說法不是很準確,可是要搞明白這裏面的細節一兩篇文章講不清楚 ~_~,咱們暫且這麼認爲吧。)數組

上面咱們也看到了,咱們在native層替換的那個 ArtMethod 不是在 Java 層也有對應的東西麼?咱們直接替換掉 Java 層的這個artMethod 字段不就OK了?可是咱們要注意的是,在Java裏面除了基本類型,其餘東西都是引用。要實現相似C++裏面那種替換引用所指向內容的機智,須要一些黑科技。app

Unsafe 和 Memory

要在Java層操做內容,也不是沒有辦法作到;JDK給咱們留了一個後門:sun.misc.Unsafe 類;在OpenJDK裏面這個類灰常強大,從內存操做到CAS到鎖機制,無所不能(惋惜的是聽說JDK8要去掉?)可是在Android 平臺還有一點點不同,在 Android N以前,Android的JDK實現是 Apache Harmony,這個實現裏面的Unsafe就有點雞肋了,無法寫內存;好在Android 又開了一個後門:Memory 類。框架

有了這兩個類,咱們就能在Java層進行簡單的內存操做了!!因爲這兩個類是隱藏類,我寫了一個wrapper,以下:函數

 1 private static class Memory {
 2 
 3     // libcode.io.Memory#peekByte
 4     static byte peekByte(long address) {
 5         return (Byte) Reflection.call(null, "libcore.io.Memory", "peekByte", null, new Class[]{long.class}, new Object[]{address});
 6     }
 7 
 8     static void pokeByte(long address, byte value) {
 9         Reflection.call(null, "libcore.io.Memory", "pokeByte", null, new Class[]{long.class, byte.class}, new Object[]{address, value});
10     }
11 
12     public static void memcpy(long dst, long src, long length) {
13         for (long i = 0; i < length; i++) {
14             pokeByte(dst, peekByte(src));
15             dst++;
16             src++;
17         }
18     }
19 }
20 
21 static class Unsafe {
22 
23     static final String UNSAFE_CLASS = "sun.misc.Unsafe";
24     static Object THE_UNSAFE;
25 
26     private static boolean is64Bit;
27 
28     static {
29         THE_UNSAFE = Reflection.get(null, UNSAFE_CLASS, "THE_ONE", null);
30         Object runtime = Reflection.call(null, "dalvik.system.VMRuntime", "getRuntime", null, null, null);
31         is64Bit = (Boolean) Reflection.call(null, "dalvik.system.VMRuntime", "is64Bit", runtime, null, null);
32     }
33 
34     public static long getObjectAddress(Object o) {
35         Object[] objects = {o};
36         Integer baseOffset = (Integer) Reflection.call(null, UNSAFE_CLASS,
37                 "arrayBaseOffset", THE_UNSAFE, new Class[]{Class.class}, new Object[]{Object[].class});
38         return ((Number) Reflection.call(null, UNSAFE_CLASS, is64Bit ? "getLong" : "getInt", THE_UNSAFE,
39                 new Class[]{Object.class, long.class}, new Object[]{objects, baseOffset.longValue()})).longValue();
40     }
41 }

 

具體實現

接下來思路就很簡單了呀,用僞代碼表示就是:  

1 memcopy(originArtMethod, replaceArtMethod);

可是事情沒有一個 sizeof 那麼簡單。你看AndFix的實現是在每一個Android版本把ArtMethod這個結構體複製一份的;要想用sizeof還得把這個類全部的引用複製過來,及其麻煩。更況且在Java裏面 sizeof都沒有。不過也不是沒有辦法,既然咱們已經能在Java層拿到對象的地址,只須要建立一個數組,丟兩個ArtMethod,把兩個數組元素的起始地址相減不就獲得一個 artMethod的大小了嗎?(此方法來自Android熱修復升級探索——追尋極致的代碼熱替換)可是還有一個問題,咱們要整個把 originMethod 的 artMethod 所在的內存直接替換爲 replaceMethod 的artMethod 所在的內存(上面咱們已經知道,Java層Method類的artMethod實際上就是native層的指針表示,在Android N上更明顯,這玩意兒直接就是一個long),如今咱們已經知道這兩個地址是什麼,那麼咱們把 replaceArtMethod 表明的內存複製到 originArtMethod 的區域,應該還須要知道一個 artMethod 有多大。

不過,既然咱們實現了方法替換;還有最後一個問題,若是咱們須要在替換後的方法裏面調用原函數呢?這個也很簡單,咱們只須要把原函數copy一份保存起來,須要調用原函數的時候調用那個copy的函數不就好了?不過在具體實現的時候,會遇到一個問題,就是 Java的非static 非private的方法默認是虛方法,在調用這個方法的時候會有一個相似查找虛函數表的過程,這個在上面的代碼 InvokeMethod 裏面能夠看到:  

 1 mirror::Object* receiver = nullptr;
 2 if (!m->IsStatic()) {
 3   // Check that the receiver is non-null and an instance of the field's declaring class.
 4   receiver = soa.Decode<mirror::Object*>(javaReceiver);
 5   if (!VerifyObjectIsClass(receiver, declaring_class)) {
 6     return NULL;
 7   }
 8 
 9   // Find the actual implementation of the virtual method.
10   m = receiver->GetClass()->FindVirtualMethodForVirtualOrInterface(m);
11 }

詳細代碼見:github/epic

在調用的時候,若是不是static的方法,會去查找這個方法的真正實現;咱們直接把原方法作了備份以後,去調用備份的那個方法,若是此方法是public的,則會查找到原來的那個函數,因而就無限循環了;咱們只須要阻止這個過程,查看 FindVirtualMethodForVirtualOrInterface 這個方法的實現就知道,只要方法是 invoke-direct 進行調用的,就會直接返回原方法,這些方法包括:構造函數,private的方法( 見 https://source.android.com/devices/tech/dalvik/dalvik-bytecode.html) 所以,咱們手動把這個備份的方法屬性修改成private便可解決這個問題。

相關文章
相關標籤/搜索