ART深度探索開篇:從Method Hook談起

bobo_java 7年前發布 | 5K 次閱讀 Java 安卓開發 Android開發 移動開發

Android上的熱修復框架 AndFix 想必已經是耳熟能詳,它的原理實際上很簡單:方法替換——Java層的每一個方法在虛擬機實現里面都對應著一個ArtMethod的結構體,只要把原方法的結構體內容替換成新的結構體的內容,在調用原方法的時候,真正執行的指令會是新方法的指令;

為什么可以這么做呢?那得從 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) 有什么區別嗎?這個問題后續再談)

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

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

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指針,我們跟進去看看是怎么實現的:

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,如下:

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();
    }
}

具體實現

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

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的大小了嗎?

不過,既然我們實現了方法替換;還有最后一個問題,如果我們需要在替換后的方法里面調用原函數呢?這個也很簡單,我們只需要把原函數copy一份保存起來,需要調用原函數的時候調用那個copy的函數不就行了?不過在具體實現的時候,會遇到一個問題,就是 Java的非static 非private的方法默認是虛方法,在調用這個方法的時候會有一個類似查找虛函數表的過程,這個在上面的代碼 InvokeMethod 里面可以看到:

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的方法, 因此,我們手動把這個備份的方法屬性修改為private即可解決這個問題。

至此,我們就用純Java實現了一個 AndFix,代碼只有200行不到!!是不是很神奇?當然,這里面包含了很多黑科技,接下來我們將以這個為引子,深入探索Android ART的方方面面,揭開虛擬機底層的神秘面紗,敬請期待~~

 

來自:http://weishu.me/2017/03/20/dive-into-art-hello-world/

 

 本文由用戶 bobo_java 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!