java.lang.VerifyError:一技組合拳

0x00 背景

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

0x01 問題定位

VerifyError 錯誤一般出現(xiàn)的 5.0 以下。通常由分包導致的。但是這次發(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 虛擬機校驗寄存器注冊表。

根源問題在寄存器類型匹配失敗。 導致校驗方法失敗從而校驗類失敗。

比較吊詭的是這個問題只出現(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 認為 move-exception 指令是一個無意義的操作,該指令將會被移除。

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

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 標記。當對應(yīng)的 insn_flags 設(shè)置為 Changed。 那么該指令需要被校驗。art 會從第一個指令開始校驗 。 校驗指令的同時會設(shè)置其他的指令設(shè)置 Changed。如操作指令會設(shè)置下一個指令為 Changed。分支指令因為存在多個分支的指令。 會對多個分支的第一個指令設(shè)置 Changed。回值指令 則不會為任何指令設(shè)置。 通過檢查是否還存在 Changed 標記位來檢查是否完成校驗工作。
關(guān)于指令的類型定義都 dex_instruction_list.h

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

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

從字節(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
    復制寄存注冊表到 000e 上

  6. 第六步
    校驗指令 0007: invoke-virtual {v4}, Landroid/app/Activity;.isInMultiWindowMode:()Z
    檢驗發(fā)現(xiàn) isInMultiWindowMode 方法不存在。該異常會導致出現(xiàn)運行期異常。 該條鏈路以下的指令不再校驗。 不再為任何指令設(shè)置 Changed 。
    當前寄存器注冊表 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 。 由于第六步的異常導致所有的指令都強制設(shè)置為會發(fā)生異常。 導致 art 錯誤的把一個未賦值的 saved_line_ 寄存器注冊表賦值給 0x001a ,同時設(shè)置 0x001a 的 insn_flags 設(shè)置為 Changed 。
    執(zhí)行指令是否會發(fā)生異常查看 dex_instruction_list.h kThrow

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

異?,F(xiàn)場復現(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)化力度變?。?/p>

  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 被修復。
修復的 commit 如下:

  1. saved_line_ 正確被賦值
    d7f8d059 diff

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容