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

Android上的熱修復(fù)框架 AndFix 想必已經(jīng)是耳熟能詳俐镐,它的原理實(shí)際上很簡單:方法替換——Java層的每一個方法在虛擬機(jī)實(shí)現(xiàn)里面都對應(yīng)著一個ArtMethod的結(jié)構(gòu)體份蝴,只要把原方法的結(jié)構(gòu)體內(nèi)容替換成新的結(jié)構(gòu)體的內(nèi)容,在調(diào)用原方法的時候,真正執(zhí)行的指令會是新方法的指令;這樣就能實(shí)現(xiàn)熱修復(fù),詳細(xì)代碼見 AndFix步鉴。

為什么可以這么做呢?那得從 Android 虛擬機(jī)的方法調(diào)用過程說起蛮浑。作為一個系列的開篇唠叛,本文不打算展開講虛擬機(jī)原理等內(nèi)容,首先給大家一道開胃菜沮稚;后續(xù)我們再深入探索ART艺沼。

眾所周知,AndFix是一種 native 的hotfix方案蕴掏,它的替換過程是用 c 在 native層完成的障般,但其實(shí),我們也可以用純Java實(shí)現(xiàn)它盛杰!而且挽荡,代碼還非常精簡,且看——

方法替換原理

既然我們知道 AndFix 的原理是方法替換即供,那么為什么直接替換Java里面的 java.lang.reflect.Method 有什么問題嗎定拟?直接這樣貌似很難下結(jié)論,那我們換個思路逗嫡。我們實(shí)現(xiàn)方法替換的結(jié)果青自,就是調(diào)用原方法的時候最終是調(diào)用被替換的方法株依。因此,我們可以看看 java.lang.reflect.Method類的 invoke 方法延窜。(這里有個疑問偶宫,F(xiàn)oo.bar()這種直接調(diào)用與反射調(diào)用Foo.class.getDeclaredMethod("bar").invoke(null) 有什么區(qū)別嗎桑孩?這個問題后續(xù)再談)

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

這個invoke是一個native方法,它的native實(shí)現(xiàn)在 art/runtime/native/java_lang_reflect_Method.cc 里面啡省,這個jni方法最終調(diào)用了 art/runtime/reflection.ccInvokeMethod方法:

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

上面函數(shù) InvokeMethod 的第二個參數(shù) javaMethod 就是Java層我們進(jìn)行反射調(diào)用的那個Method對象谋减,在jni層反映為一個jobject牡彻;InvokeMethod這個native方法首先通過 mirror::ArtMethod::FromReflectedMethod 獲取了Java對象的在native層的 ArtMethod指針扫沼,我們跟進(jìn)去看看是怎么實(shí)現(xiàn)的:

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

我們在這里看到了一點(diǎn)端倪出爹,獲取到了Java層那個Method對象的一個叫做 artMethod的字段,然后強(qiáng)轉(zhuǎn)成了ArtMethod指針(這里的說法不是很準(zhǔn)確缎除,但是要搞明白這里面的細(xì)節(jié)一兩篇文章講不清楚 _严就,我們暫且這么認(rèn)為吧。)

AndFix的實(shí)現(xiàn)里面器罐,也正是使用這個 FromReflectedMethod 方法拿到Java層Method對應(yīng)native層的ArtMethod指針梢为,然后執(zhí)行替換的。

上面我們也看到了轰坊,我們在native層替換的那個 ArtMethod 不是在 Java 層也有對應(yīng)的東西么铸董?我們直接替換掉 Java 層的這個artMethod 字段不就OK了?但是我們要注意的是肴沫,在Java里面除了基本類型粟害,其他東西都是引用。要實(shí)現(xiàn)類似C++里面那種替換引用所指向內(nèi)容的機(jī)智颤芬,需要一些黑科技悲幅。

Unsafe 和 Memory

要在Java層操作內(nèi)容,也不是沒有辦法做到站蝠;JDK給我們留了一個后門:sun.misc.Unsafe 類汰具;在OpenJDK里面這個類灰常強(qiáng)大,從內(nèi)存操作到CAS到鎖機(jī)制菱魔,無所不能(可惜的是據(jù)說JDK8要去掉留荔?)但是在Android 平臺還有一點(diǎn)點(diǎn)不一樣,在 Android N之前澜倦,Android的JDK實(shí)現(xiàn)是 Apache Harmony聚蝶,這個實(shí)現(xiàn)里面的Unsafe就有點(diǎn)雞肋了拔疚,沒法寫內(nèi)存;好在Android 又開了一個后門:Memory 類既荚。

有了這兩個類稚失,我們就能在Java層進(jìn)行簡單的內(nèi)存操作了!恰聘!由于這兩個類是隱藏類句各,我寫了一個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();
        }
    }

具體實(shí)現(xiàn)

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

memcopy(originArtMethod, replaceArtMethod);

但是還有一個問題凿宾,我們要整個把 originMethod 的 artMethod 所在的內(nèi)存直接替換為 replaceMethod 的artMethod 所在的內(nèi)存(上面我們已經(jīng)知道,Java層Method類的artMethod實(shí)際上就是native層的指針表示兼蕊,在Android N上更明顯初厚,這玩意兒直接就是一個long),現(xiàn)在我們已經(jīng)知道這兩個地址是什么孙技,那么我們把 replaceArtMethod 代表的內(nèi)存復(fù)制到 originArtMethod 的區(qū)域产禾,應(yīng)該還需要知道一個 artMethod 有多大。

但是事情沒有一個 sizeof 那么簡單牵啦。你看AndFix的實(shí)現(xiàn)是在每個Android版本把ArtMethod這個結(jié)構(gòu)體復(fù)制一份的亚情;要想用sizeof還得把這個類所有的引用復(fù)制過來,及其麻煩哈雏。更何況在Java里面 sizeof都沒有楞件。不過也不是沒有辦法,既然我們已經(jīng)能在Java層拿到對象的地址裳瘪,只需要創(chuàng)建一個數(shù)組土浸,丟兩個ArtMethod,把兩個數(shù)組元素的起始地址相減不就得到一個 artMethod的大小了嗎彭羹?

不過黄伊,既然我們實(shí)現(xiàn)了方法替換;還有最后一個問題皆怕,如果我們需要在替換后的方法里面調(diào)用原函數(shù)呢毅舆?這個也很簡單,我們只需要把原函數(shù)copy一份保存起來愈腾,需要調(diào)用原函數(shù)的時候調(diào)用那個copy的函數(shù)不就行了憋活?不過在具體實(shí)現(xiàn)的時候,會遇到一個問題虱黄,就是 Java的非static 非private的方法默認(rèn)是虛方法悦即,在調(diào)用這個方法的時候會有一個類似查找虛函數(shù)表的過程,這個在上面的代碼 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);
  }

在調(diào)用的時候,如果不是static的方法辜梳,會去查找這個方法的真正實(shí)現(xiàn)粱甫;我們直接把原方法做了備份之后,去調(diào)用備份的那個方法作瞄,如果此方法是public的茶宵,則會查找到原來的那個函數(shù),于是就無限循環(huán)了宗挥;我們只需要阻止這個過程乌庶,查看 FindVirtualMethodForVirtualOrInterface 這個方法的實(shí)現(xiàn)就知道,只要方法是 invoke-direct 進(jìn)行調(diào)用的契耿,就會直接返回原方法瞒大,這些方法包括:構(gòu)造函數(shù),private的方法( 見 https://source.android.com/devices/tech/dalvik/dalvik-bytecode.html) 因此搪桂,我們手動把這個備份的方法屬性修改為private即可解決這個問題透敌。

詳細(xì)代碼見:github/epic

至此,我們就用純Java實(shí)現(xiàn)了一個 AndFix踢械,代碼只有200行不到P锏纭!是不是很神奇裸燎?當(dāng)然顾瞻,這里面包含了很多黑科技,接下來我們將以這個為引子德绿,深入探索Android ART的方方面面,揭開虛擬機(jī)底層的神秘面紗退渗,敬請期待~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末移稳,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子会油,更是在濱河造成了極大的恐慌个粱,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翻翩,死亡現(xiàn)場離奇詭異都许,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)嫂冻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門胶征,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人桨仿,你說我怎么就攤上這事睛低。” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵钱雷,是天一觀的道長骂铁。 經(jīng)常有香客問我,道長罩抗,這世上最難降的妖魔是什么拉庵? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮套蒂,結(jié)果婚禮上名段,老公的妹妹穿的比我還像新娘。我一直安慰自己泣懊,他們只是感情好伸辟,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著馍刮,像睡著了一般信夫。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上卡啰,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天静稻,我揣著相機(jī)與錄音,去河邊找鬼匈辱。 笑死振湾,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的亡脸。 我是一名探鬼主播押搪,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼浅碾!你這毒婦竟也來了大州?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤垂谢,失蹤者是張志新(化名)和其女友劉穎厦画,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體滥朱,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡根暑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了徙邻。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片排嫌。...
    茶點(diǎn)故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖鹃栽,靈堂內(nèi)的尸體忽然破棺而出躏率,到底是詐尸還是另有隱情躯畴,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布薇芝,位于F島的核電站蓬抄,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏夯到。R本人自食惡果不足惜嚷缭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望耍贾。 院中可真熱鬧阅爽,春花似錦、人聲如沸荐开。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽晃听。三九已至百侧,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間能扒,已是汗流浹背佣渴。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留初斑,地道東北人辛润。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像见秤,于是被迫代替她去往敵國和親砂竖。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容