ART深度探索開篇:從Method Hook談起
Android上的熱修復框架 AndFix 想必已經是耳熟能詳,它的原理實際上很簡單:方法替換——Java層的每一個方法在虛擬機器實現裡面都對應著一個ArtMethod的結構體,只要把原方法的結構體內容替換成新的結構體的內容,在呼叫原方法的時候,真正執行的指令會是新方法的指令;這樣就能實現熱修復,詳細程式碼見 AndFix。
為什麼可以這麼做呢?那得從 Android 虛擬機器的方法呼叫過程說起。作為一個系列的開篇,本文不打算展開講虛擬機器原理等內容,首先給大家一道開胃菜;後續我們再深入探索ART。
眾所周知,AndFix是一種 native 的hotfix方案,它的替換過程是用 c 在 native層完成的,但其實,我們也可以用純Java實現它!而且,程式碼還非常精簡,且看——
方法替換原理
既然我們知道 AndFix 的原理是方法替換,那麼為什麼直接替換Java裡面的 java.lang.reflect.Method
有什麼問題嗎?直接這樣貌似很難下結論,那我們換個思路。我們實現方法替換的結果,就是呼叫原方法的時候最終是呼叫被替換的方法。因此,我們可以看看 java.lang.reflect.Method
類的 invoke
方法。(這裡有個疑問,Foo.bar()這種直接呼叫與反射呼叫Foo.class.getDeclaredMethod(“bar”).invoke(null)
有什麼區別嗎?這個問題後續再談)
1 2 |
private native
|
這個invoke是一個native方法,它的native實現在 art/runtime/native/java_lang_reflect_Method.cc
裡面,這個jni方法最終呼叫了 art/runtime/reflection.cc
的 InvokeMethod
方法:
1 2 |
object InvokeMethod(const ScopedObjectAccessAlreadyRunnable& soa, jobject javaMethod, jobject javaReceiver, jobject javaArgs, bool accessible) { // 略... mirror::ArtMethod* m = mirror::ArtMethod::FromReflectedMethod(soa, javaMethod); mirror::Class* declaring_class = m->GetDeclaringClass(); // 按需初始化類,略。。 mirror::Object* receiver = nullptr; if (!m->IsStatic()) { // Check that the receiver is non-null and an instance of the field's declaring class. receiver = soa.Decode<mirror::Object*>(javaReceiver); if (!VerifyObjectIsClass(receiver, declaring_class)) { return NULL; } // Find the actual implementation of the virtual method. m = receiver->GetClass()->FindVirtualMethodForVirtualOrInterface(m); } // 略.. InvokeWithArgArray(soa, m, &arg_array, &result, shorty); // 略 。。 // Box if necessary and return. return soa.AddLocalReference<jobject>(BoxPrimitive(mh.GetReturnType()->GetPrimitiveType(), result)); } |
上面函式 InvokeMethod 的第二個引數 javaMethod
就是Java層我們進行反射呼叫的那個Method物件,在jni層反映為一個jobject;InvokeMethod這個native方法首先通過 mirror::ArtMethod::FromReflectedMethod
獲取了Java物件的在native層的
ArtMethod指標,我們跟進去看看是怎麼實現的:
1 2 3 4 5 6 7 8 |
ArtMethod* ArtMethod::FromReflectedMethod(const ScopedObjectAccessAlreadyRunnable& soa, jobject jlr_method) { mirror::ArtField* f = soa.DecodeField(WellKnownClasses::java_lang_reflect_AbstractMethod_artMethod); mirror::ArtMethod* method = f->GetObject(soa.Decode<mirror::Object*>(jlr_method))->AsArtMethod(); DCHECK(method != nullptr); return method; } |
我們在這裡看到了一點端倪,獲取到了Java層那個Method物件的一個叫做 artMethod
的欄位,然後強轉成了ArtMethod指標(這裡的說法不是很準確,但是要搞明白這裡面的細節一兩篇文章講不清楚 ~_~,我們暫且這麼認為吧。)
AndFix的實現裡面,也正是使用這個 FromReflectedMethod
方法拿到Java層Method對應native層的ArtMethod指標,然後執行替換的。
上面我們也看到了,我們在native層替換的那個 ArtMethod 不是在 Java 層也有對應的東西麼?我們直接替換掉 Java 層的這個artMethod 欄位不就OK了?但是我們要注意的是,在Java裡面除了基本型別,其他東西都是引用。要實現類似C++裡面那種替換引用所指向內容的機智,需要一些黑科技。
Unsafe 和 Memory
要在Java層操作內容,也不是沒有辦法做到;JDK給我們留了一個後門:sun.misc.Unsafe
類;在OpenJDK裡面這個類灰常強大,從記憶體操作到CAS到鎖機制,無所不能(可惜的是據說JDK8要去掉?)但是在Android 平臺還有一點點不一樣,在
Android N之前,Android的JDK實現是 Apache Harmony,這個實現裡面的Unsafe就有點雞肋了,沒法寫記憶體;好在Android 又開了一個後門:Memory
類。
有了這兩個類,我們就能在Java層進行簡單的記憶體操作了!!由於這兩個類是隱藏類,我寫了一個wrapper,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
private static class Memory { // libcode.io.Memory#peekByte static byte peekByte(long address) { return (Byte) Reflection.call(null, "libcore.io.Memory", "peekByte", null, new Class[]{long.class}, new Object[]{address}); } static void pokeByte(long address, byte value) { Reflection.call(null, "libcore.io.Memory", "pokeByte", null, new Class[]{long.class, byte.class}, new Object[]{address, value}); } public static void memcpy(long dst, long src, long length) { for (long i = 0; i < length; i++) { pokeByte(dst, peekByte(src)); dst++; src++; } } } static class Unsafe { static final String UNSAFE_CLASS = "sun.misc.Unsafe"; static Object THE_UNSAFE; private static boolean is64Bit; static { THE_UNSAFE = Reflection.get(null, UNSAFE_CLASS, "THE_ONE", null); Object runtime = Reflection.call(null, "dalvik.system.VMRuntime", "getRuntime", null, null, null); is64Bit = (Boolean) Reflection.call(null, "dalvik.system.VMRuntime", "is64Bit", runtime, null, null); } public static long getObjectAddress(Object o) { Object[] objects = {o}; Integer baseOffset = (Integer) Reflection.call(null, UNSAFE_CLASS, "arrayBaseOffset", THE_UNSAFE, new Class[]{Class.class}, new Object[]{Object[].class}); return ((Number) Reflection.call(null, UNSAFE_CLASS, is64Bit ? "getLong" : "getInt", THE_UNSAFE, new Class[]{Object.class, long.class}, new Object[]{objects, baseOffset.longValue()})).longValue(); } } |
具體實現
接下來思路就很簡單了呀,用偽程式碼表示就是:
1
|
memcopy(originArtMethod, replaceArtMethod);
|
但是還有一個問題,我們要整個把 originMethod 的 artMethod 所在的記憶體直接替換為 replaceMethod 的artMethod 所在的記憶體(上面我們已經知道,Java層Method類的artMethod實際上就是native層的指標表示,在Android N上更明顯,這玩意兒直接就是一個long),現在我們已經知道這兩個地址是什麼,那麼我們把 replaceArtMethod 代表的記憶體複製到 originArtMethod 的區域,應該還需要知道一個 artMethod 有多大。
但是事情沒有一個 sizeof 那麼簡單。你看AndFix的實現是在每個Android版本把ArtMethod這個結構體複製一份的;要想用sizeof還得把這個類所有的引用複製過來,及其麻煩。更何況在Java裡面 sizeof都沒有。不過也不是沒有辦法,既然我們已經能在Java層拿到物件的地址,只需要建立一個數組,丟兩個ArtMethod,把兩個陣列元素的起始地址相減不就得到一個 artMethod的大小了嗎?(此方法來自Android熱修復升級探索——追尋極致的程式碼熱替換)
不過,既然我們實現了方法替換;還有最後一個問題,如果我們需要在替換後的方法裡面呼叫原函式呢?這個也很簡單,我們只需要把原函式copy一份儲存起來,需要呼叫原函式的時候呼叫那個copy的函式不就行了?不過在具體實現的時候,會遇到一個問題,就是 Java的非static 非private的方法預設是虛方法,在呼叫這個方法的時候會有一個類似查詢虛擬函式表的過程,這個在上面的程式碼 InvokeMethod
裡面可以看到:
1 2 3 4 5 6 7 8 9 10 11 |
mirror::Object* receiver = nullptr; if (!m->IsStatic()) { // Check that the receiver is non-null and an instance of the field's declaring class. receiver = soa.Decode<mirror::Object*>(javaReceiver); if (!VerifyObjectIsClass(receiver, declaring_class)) { return NULL; } // Find the actual implementation of the virtual method. m = receiver->GetClass()->FindVirtualMethodForVirtualOrInterface(m); } |
在呼叫的時候,如果不是static的方法,會去查詢這個方法的真正實現;我們直接把原方法做了備份之後,去呼叫備份的那個方法,如果此方法是public的,則會查詢到原來的那個函式,於是就無限迴圈了;我們只需要阻止這個過程,檢視 FindVirtualMethodForVirtualOrInterface 這個方法的實現就知道,只要方法是 invoke-direct 進行呼叫的,就會直接返回原方法,這些方法包括:建構函式,private的方法( 見 https://source.android.com/devices/tech/dalvik/dalvik-bytecode.html) 因此,我們手動把這個備份的方法屬性修改為private即可解決這個問題。
至此,我們就用純Java實現了一個 AndFix,程式碼只有200行不到!!是不是很神奇?當然,這裡麵包含了很多黑科技,接下來我們將以這個為引子,深入探索Android ART的方方面面,揭開虛擬機器底層的神祕面紗,敬請期待~~
原文地址: http://weishu.me/2017/03/20/dive-into-art-hello-world/