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)兩個異常信息:
- isInMultiWindowMode 方法未找到 :
找不到 isInMultiWindowMode 方法蚣驼。 這個方法是在 api 24 上加入的, 確實在 android 5.1 ( api 22) 上不存在。 但就這相艇? - 寄存器類型匹配失敗:
java 虛擬機檢驗類合法性的時候會匹配棧幀颖杏。 對應(yīng) android 虛擬機校驗寄存器注冊表。
根源問題在寄存器類型匹配失敗坛芽。 導(dǎo)致校驗方法失敗從而校驗類失敗留储。
比較吊詭的是這個問題只出現(xiàn)在 android 5.1 上。 并且只在 Release 包上出現(xiàn)咙轩。 據(jù)其原因我們使用 dexduup 工具 查看該方法在 Debug 和 Release 包生成的 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);
}
校驗方法主要以下幾個方面
- 校驗指令大小是否超過聲明大小稀火。
- 校驗方法指令使用的寄存器是否越界暖哨。
- 校驗跳轉(zhuǎn)指令是否越界或錯誤
- 校驗指令引用的元素在 Dex 位置是否正確
- 校驗寄存器注冊表否正確。即從寄存器讀取的類型是否匹配聲明的類型凰狞。
- 鎖 是否被正確釋放篇裁。
這次這個錯誤是在校驗寄存器注冊表出現(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
第一步
該方法聲明寄存器5個授霸,初始化寄存器注冊表 V0~V4: xxxL1L2
x: 未定義
L1 :this 對象類型
L2 :第一個入?yún)?/p>第二步
校驗第一個指令 0000 sget V0
設(shè)置指令 0002 的 insn_flags 為 Changed
寄存器注冊表 IxxL1L2第三步
校驗指令 0002 const/4 v1, #int 0
設(shè)置下一個指令 0003 的 insn_flags 為 Changed
寄存器注冊表 IIxL1L2第四步
校驗指令 0003 const/16 v2, #int 24
設(shè)置下一個指令 0005 的 insn_flags 為 Changed
寄存器注冊表 IIIL1L2第五步
校驗分支指令 0005: if-lt v0, v2, 000e
設(shè)置下一個指令 0007 的 insn_flags 為 Changed
設(shè)置下個分支第一個指令 000e 的 insn_flags 為 Changed
寄存器注冊表 IIIL1L2
復(fù)制寄存注冊表到 000e 上第六步
校驗指令 0007: invoke-virtual {v4}, Landroid/app/Activity;.isInMultiWindowMode:()Z
檢驗發(fā)現(xiàn) isInMultiWindowMode 方法不存在巡验。該異常會導(dǎo)致出現(xiàn)運行期異常。 該條鏈路以下的指令不再校驗碘耳。 不再為任何指令設(shè)置 Changed 显设。
當(dāng)前寄存器注冊表 IIIL1L2第七步
由于 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第八步
檢驗 001a: return v1隙弛。 檢驗寄存器1
由于當(dāng)前寄存器注冊表未賦值為 xxxxx
校驗失敗架馋。結(jié)束校驗。拋出異常
異橙疲現(xiàn)場復(fù)現(xiàn)绩蜻。
0x03 總結(jié)
Bug 如何出現(xiàn) ?
這個 Bug 是一套組合室埋。
- 一個運行期異常办绝。
- 緊跟一個 try catch 代碼塊
- try catch 第一個指令運行不會發(fā)生異常
- catch異常處理第一個指令是一個從寄存器讀的操作。
如何解決這個 Bug 姚淆?
棄用 D8 使用 dx 來轉(zhuǎn)化 Dex (歷史的倒退)
棄用 release 模式的 D8 來生成 Dex(優(yōu)化力度變性胁酢)
規(guī)避特定的排序。 (看天吃飯)
節(jié)點1 去除 isInMultiWindowMode 方法調(diào)用腌逢。
節(jié)點2 關(guān)閉強轉(zhuǎn)降淮。
節(jié)點3 處理異常。
節(jié)點3 return 非 0 搏讶。對 D8 進行干預(yù)佳鳖。 關(guān)閉 move-exception 指令的優(yōu)化
MoveException.java
Bug 影響范圍 ?
問題存在在 api 21-22 在 api 23 被修復(fù)媒惕。
修復(fù)的 commit 如下:
saved_line_ 正確被賦值
d7f8d059 diffhave_pending_runtime_throw_failure_ 狀態(tài)及時重置系吩。
3ae8da0 diff