java.lang.VerifyError:一技組合拳

0x00 背景

最近整體升級了項目的工具鏈怕犁。 使用了 D8 作為項目的主力。
在 Release 包在 5.1 上出現(xiàn)了 java.lang.VerifyError 異常慷荔。

0x01 問題定位

VerifyError 錯誤一般出現(xiàn)的 5.0 以下耗跛。通常由分包導(dǎo)致的。但是這次發(fā)生的機子是 5.1 节芥。

我們將問題代碼進行簡化如下在刺。

public class A {

    // 方法調(diào)用入口
    public int method1(Activity activity) {
        if (Build.VERSION.SDK_INT >= 24 && activity.isInMultiWindowMode()) {
            // 節(jié)點1 
            return 0;
        }
        try {
            // 節(jié)點2 
            Point screenSize = method2((Runnable) activity);
            method3(activity, screenSize);
            return 1;
        } catch (Exception e) {
            // 節(jié)點3 
            return 0;
        }
    }

    private Point method2(Runnable activity) {
        return new Point();
    }

    private void method3(Activity activity, Point screenSize) {
        //忽略
    }
}

運行奔潰如下:

  java.lang.VerifyError: Verifier rejected class com.dim.A due to bad method int com.dim.A.method1(android.app.Activity) (declaration of 'com.dim.A' appears in /data/app/com.dim-2/base.apk)

往往單純的奔潰信息是不足以發(fā)現(xiàn)問題的。查找上下文日志獲取更多信息头镊。

 I/art: Verification error in int com.dim.A.method1(android.app.Activity)
 I/art: int com.dim.A.method1(android.app.Activity): [0x7] couldn't find method android.app.Activity.isInMultiWindowMode ()Z
 I/art: int com.dim.A.method1(android.app.Activity) failed to verify: int com.dim.A.method1(android.app.Activity): [0x1A] register v1 has type Undefined but expected Integer return-1nr on invalid register v1 
 E/art: Verification failed on class com.dim.A in /data/app/com.dim-2/base.apk because: Verifier rejected class com.dim.A due to bad method int com.dim.A.method1(android.app.Activity)

發(fā)現(xiàn)兩個異常信息:

  1. isInMultiWindowMode 方法未找到 :
    找不到 isInMultiWindowMode 方法蚣驼。 這個方法是在 api 24 上加入的, 確實在 android 5.1 ( api 22) 上不存在。 但就這相艇?
  2. 寄存器類型匹配失敗:
    java 虛擬機檢驗類合法性的時候會匹配棧幀颖杏。 對應(yīng) android 虛擬機校驗寄存器注冊表。

根源問題在寄存器類型匹配失敗坛芽。 導(dǎo)致校驗方法失敗從而校驗類失敗留储。

比較吊詭的是這個問題只出現(xiàn)在 android 5.1 上。 并且只在 Release 包上出現(xiàn)咙轩。 據(jù)其原因我們使用 dexduup 工具 查看該方法在 Debug 和 Release 包生成的 Dex 字節(jié)碼的異同获讳。

Dex字節(jié)碼異同

可以看出方法使用的寄存器 5 個。一個 catch 異常處理活喊。參數(shù)2個丐膝。 Debug 包僅僅比 Release 包在異常處理處多個一個 move-exception 指令。

字節(jié)碼的異同是因為項目中使用 D8 钾菊。D8 生成 Dex 的時候會做一些優(yōu)化帅矗。如字符串優(yōu)化, new-array 指令優(yōu)化,分支指令優(yōu)化等结缚。 其中包含一些無效指令的刪除损晤。 比如一個異常被 catch。 但并沒有對異常進行操作红竭。在 Release 模式下那么 D8 認(rèn)為 move-exception 指令是一個無意義的操作尤勋,該指令將會被移除。

至此我們已經(jīng)知道了出現(xiàn)問題的大概茵宪。
因為 D8 對 Dex 優(yōu)化最冰。生成特定的指令排列導(dǎo)致在部分虛擬機校驗失敗。

0x02 問題回朔

查看 art 相關(guān)代碼
art 方法校驗入口在 MethodVerifier::Verify()


  insn_flags_.reset(new InstructionFlags[code_item_->insns_size_in_code_units_]());
  // Run through the instructions and see if the width checks out.
  bool result = ComputeWidthsAndCountOps();
  // Flag instructions guarded by a "try" block and check exception handlers.
  result = result && ScanTryCatchBlocks();
  // Perform static instruction verification.
  result = result && VerifyInstructions();
  // Perform code-flow analysis and return.
  result = result && VerifyCodeFlow();
  // Compute information for compiler.
  if (result && Runtime::Current()->IsCompiler()) {
    result = Runtime::Current()->GetCompilerCallbacks()->MethodVerified(this);
  }

校驗方法主要以下幾個方面

  1. 校驗指令大小是否超過聲明大小稀火。
  2. 校驗方法指令使用的寄存器是否越界暖哨。
  3. 校驗跳轉(zhuǎn)指令是否越界或錯誤
  4. 校驗指令引用的元素在 Dex 位置是否正確
  5. 校驗寄存器注冊表否正確。即從寄存器讀取的類型是否匹配聲明的類型凰狞。
  6. 鎖 是否被正確釋放篇裁。

這次這個錯誤是在校驗寄存器注冊表出現(xiàn)的沛慢。

寄存注冊表校驗流程如下:

為每個指令設(shè)置一個 insn_flags 標(biāo)記。當(dāng)對應(yīng)的 insn_flags 設(shè)置為 Changed达布。 那么該指令需要被校驗团甲。art 會從第一個指令開始校驗 。 校驗指令的同時會設(shè)置其他的指令設(shè)置 Changed黍聂。如操作指令會設(shè)置下一個指令為 Changed躺苦。分支指令因為存在多個分支的指令。 會對多個分支的第一個指令設(shè)置 Changed产还。回值指令 則不會為任何指令設(shè)置匹厘。 通過檢查是否還存在 Changed 標(biāo)記位來檢查是否完成校驗工作。
關(guān)于指令的類型定義都 dex_instruction_list.h

kContinue操作指令
kBranch分支指令
kReturn回值指令

指令在運行的時候還存在一個寄存器注冊表脐区。寄存器注冊表很大一部分體現(xiàn)了當(dāng)前運行的環(huán)境愈诚。 當(dāng)遇到分支指令的時候, 由于存在分支跳轉(zhuǎn)坡椒。還需要把寄存器注冊表狀態(tài)轉(zhuǎn)移到所有的分支上扰路。 一個指令多次被執(zhí)行的時候尤溜。就會存在多張寄存器注冊表倔叼,需要合并這些表。當(dāng)合并不兼容的時候宫莱, 需要重新校驗該分支的代碼丈攒。

從字節(jié)碼流程中觀察寄存器注冊表的變化。來定位問題

|0000: sget v0, Landroid/os/Build$VERSION;.SDK_INT:I // field@0000
|0002: const/4 v1, #int 0 // #0
|0003: const/16 v2, #int 24 // #18
|0005: if-lt v0, v2, 000e // +0009
|0007: invoke-virtual {v4}, Landroid/app/Activity;.isInMultiWindowMode:()Z // method@0001
|000a: move-result v0
|000b: if-eqz v0, 000e // +0003
|000d: return v1
|000e: move-object v0, v4
|000f: check-cast v0, Ljava/lang/Runnable; // type@001c
|0011: invoke-virtual {v3, v0}, Lcom/dim/A;.method2:(Ljava/lang/Runnable;)Landroid/graphics/Point; // method@0008
|0014: move-result-object v0
|0015: invoke-direct {v3, v4, v0}, Lcom/dim/A;.method3:(Landroid/app/Activity;Landroid/graphics/Point;)V // 
|0018: const/4 v1, #int 1 // #1
|0019: return v1
|001a: return v1
catches       : 1
    0x000e - 0x0018
    Ljava/lang/Exception; -> 0x001a

  1. 第一步
    該方法聲明寄存器5個授霸,初始化寄存器注冊表 V0~V4: xxxL1L2
    x: 未定義
    L1 :this 對象類型
    L2 :第一個入?yún)?/p>

  2. 第二步
    校驗第一個指令 0000 sget V0
    設(shè)置指令 0002 的 insn_flags 為 Changed
    寄存器注冊表 IxxL1L2

  3. 第三步
    校驗指令 0002 const/4 v1, #int 0
    設(shè)置下一個指令 0003 的 insn_flags 為 Changed
    寄存器注冊表 IIxL1L2

  4. 第四步
    校驗指令 0003 const/16 v2, #int 24
    設(shè)置下一個指令 0005 的 insn_flags 為 Changed
    寄存器注冊表 IIIL1L2

  5. 第五步
    校驗分支指令 0005: if-lt v0, v2, 000e
    設(shè)置下一個指令 0007 的 insn_flags 為 Changed
    設(shè)置下個分支第一個指令 000e 的 insn_flags 為 Changed
    寄存器注冊表 IIIL1L2
    復(fù)制寄存注冊表到 000e 上

  6. 第六步
    校驗指令 0007: invoke-virtual {v4}, Landroid/app/Activity;.isInMultiWindowMode:()Z
    檢驗發(fā)現(xiàn) isInMultiWindowMode 方法不存在巡验。該異常會導(dǎo)致出現(xiàn)運行期異常。 該條鏈路以下的指令不再校驗碘耳。 不再為任何指令設(shè)置 Changed 显设。
    當(dāng)前寄存器注冊表 IIIL1L2

  7. 第七步
    由于 000e 的 insn_flags 還是 Changed。還需要校驗指令 000e 指令
    校驗指令 000e: move-object v0, v4
    0x00e - 0x0018 是位于 try catch 里面的指令辛辨。 try catch 里所有可能發(fā)生異常的指令捕捂。都會走到 catch 的處理邏輯中。 所以需要把進入該指令前的寄存器注冊表狀態(tài)轉(zhuǎn)移到 0x001a 中斗搞。進入前的寄存器注冊表保存在 saved_line_ 變量上指攒。理論上 move-object 指令是不會發(fā)生異常的。 但是 api 22 存在的一個 bug 僻焚。 由于第六步的異常導(dǎo)致所有的指令都強制設(shè)置為會發(fā)生異常允悦。 導(dǎo)致 art 錯誤的把一個未賦值的 saved_line_ 寄存器注冊表賦值給 0x001a ,同時設(shè)置 0x001a 的 insn_flags 設(shè)置為 Changed 虑啤。
    執(zhí)行指令是否會發(fā)生異常查看 dex_instruction_list.h kThrow

  8. 第八步
    檢驗 001a: return v1隙弛。 檢驗寄存器1
    由于當(dāng)前寄存器注冊表未賦值為 xxxxx
    校驗失敗架馋。結(jié)束校驗。拋出異常

異橙疲現(xiàn)場復(fù)現(xiàn)绩蜻。

0x03 總結(jié)

Bug 如何出現(xiàn) ?

這個 Bug 是一套組合室埋。

  1. 一個運行期異常办绝。
  2. 緊跟一個 try catch 代碼塊
  3. try catch 第一個指令運行不會發(fā)生異常
  4. catch異常處理第一個指令是一個從寄存器讀的操作。

如何解決這個 Bug 姚淆?

  1. 棄用 D8 使用 dx 來轉(zhuǎn)化 Dex (歷史的倒退)

  2. 棄用 release 模式的 D8 來生成 Dex(優(yōu)化力度變性胁酢)

  3. 規(guī)避特定的排序。 (看天吃飯)
    節(jié)點1 去除 isInMultiWindowMode 方法調(diào)用腌逢。
    節(jié)點2 關(guān)閉強轉(zhuǎn)降淮。
    節(jié)點3 處理異常。
    節(jié)點3 return 非 0 搏讶。

  4. 對 D8 進行干預(yù)佳鳖。 關(guān)閉 move-exception 指令的優(yōu)化
    MoveException.java

image.png

Bug 影響范圍 ?

問題存在在 api 21-22 在 api 23 被修復(fù)媒惕。
修復(fù)的 commit 如下:

  1. saved_line_ 正確被賦值
    d7f8d059 diff

  2. have_pending_runtime_throw_failure_ 狀態(tài)及時重置系吩。
    3ae8da0 diff

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市妒蔚,隨后出現(xiàn)的幾起案子穿挨,更是在濱河造成了極大的恐慌,老刑警劉巖肴盏,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件科盛,死亡現(xiàn)場離奇詭異,居然都是意外死亡菜皂,警方通過查閱死者的電腦和手機贞绵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恍飘,“玉大人榨崩,你說我怎么就攤上這事〕B拢” “怎么了蜡饵?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長胳施。 經(jīng)常有香客問我溯祸,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任焦辅,我火速辦了婚禮博杖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘筷登。我一直安慰自己剃根,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布前方。 她就那樣靜靜地躺著狈醉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪惠险。 梳的紋絲不亂的頭發(fā)上苗傅,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天,我揣著相機與錄音班巩,去河邊找鬼渣慕。 笑死,一個胖子當(dāng)著我的面吹牛抱慌,可吹牛的內(nèi)容都是我干的逊桦。 我是一名探鬼主播,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼抑进,長吁一口氣:“原來是場噩夢啊……” “哼强经!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起单匣,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤夕凝,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后户秤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡逮矛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年鸡号,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片须鼎。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡鲸伴,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出晋控,到底是詐尸還是另有隱情汞窗,我是刑警寧澤,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布赡译,位于F島的核電站仲吏,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜裹唆,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一誓斥、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧许帐,春花似錦劳坑、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至循帐,卻和暖如春蔑穴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背惧浴。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工存和, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人衷旅。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓捐腿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親柿顶。 傳聞我的和親對象是個殘疾皇子茄袖,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,086評論 2 355