java.lang.VerifyError:一技组合拳

0x00 背景

最近整体升级了项目的工具链。 使用了 D8 作为项目的主力。
在 Release 包在 5.1 上出现了 java.lang.VerifyError 异常。

0x01 问题定位

VerifyError 错误一般出现的 5.0 以下。通常由分包导致的。但是这次发生的机子是 5.1 。

我们将问题代码进行简化如下。

public class A {

    // 方法调用入口
    public int method1(Activity activity) {
        if (Build.VERSION.SDK_INT >= 24 && activity.isInMultiWindowMode()) {
            // 节点1 
            return 0;
        }
        try {
            // 节点2 
            Point screenSize = method2((Runnable) activity);
            method3(activity, screenSize);
            return 1;
        } catch (Exception e) {
            // 节点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)

往往单纯的奔溃信息是不足以发现问题的。查找上下文日志获取更多信息。

 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)

发现两个异常信息:

  1. isInMultiWindowMode 方法未找到 :
    找不到 isInMultiWindowMode 方法。 这个方法是在 api 24 上加入的, 确实在 android 5.1 ( api 22) 上不存在。 但就这?
  2. 寄存器类型匹配失败:
    java 虚拟机检验类合法性的时候会匹配栈帧。 对应 android 虚拟机校验寄存器注册表。

根源问题在寄存器类型匹配失败。 导致校验方法失败从而校验类失败。

比较吊诡的是这个问题只出现在 android 5.1 上。 并且只在 Release 包上出现。 据其原因我们使用 dexduup 工具 查看该方法在 Debug 和 Release 包生成的 Dex 字节码的异同。

Dex字节码异同

可以看出方法使用的寄存器 5 个。一个 catch 异常处理。参数2个。 Debug 包仅仅比 Release 包在异常处理处多个一个 move-exception 指令。

字节码的异同是因为项目中使用 D8 。D8 生成 Dex 的时候会做一些优化。如字符串优化, new-array 指令优化,分支指令优化等。 其中包含一些无效指令的删除。 比如一个异常被 catch。 但并没有对异常进行操作。在 Release 模式下那么 D8 认为 move-exception 指令是一个无意义的操作,该指令将会被移除。

至此我们已经知道了出现问题的大概。
因为 D8 对 Dex 优化。生成特定的指令排列导致在部分虚拟机校验失败。

0x02 问题回朔

查看 art 相关代码
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. 校验跳转指令是否越界或错误
  4. 校验指令引用的元素在 Dex 位置是否正确
  5. 校验寄存器注册表否正确。即从寄存器读取的类型是否匹配声明的类型。
  6. 锁 是否被正确释放。

这次这个错误是在校验寄存器注册表出现的。

寄存注册表校验流程如下:

为每个指令设置一个 insn_flags 标记。当对应的 insn_flags 设置为 Changed。 那么该指令需要被校验。art 会从第一个指令开始校验 。 校验指令的同时会设置其他的指令设置 Changed。如操作指令会设置下一个指令为 Changed。分支指令因为存在多个分支的指令。 会对多个分支的第一个指令设置 Changed。回值指令 则不会为任何指令设置。 通过检查是否还存在 Changed 标记位来检查是否完成校验工作。
关于指令的类型定义都 dex_instruction_list.h

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

指令在运行的时候还存在一个寄存器注册表。寄存器注册表很大一部分体现了当前运行的环境。 当遇到分支指令的时候, 由于存在分支跳转。还需要把寄存器注册表状态转移到所有的分支上。 一个指令多次被执行的时候。就会存在多张寄存器注册表,需要合并这些表。当合并不兼容的时候, 需要重新校验该分支的代码。

从字节码流程中观察寄存器注册表的变化。来定位问题

|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 :第一个入参

  2. 第二步
    校验第一个指令 0000 sget V0
    设置指令 0002 的 insn_flags 为 Changed
    寄存器注册表 IxxL1L2

  3. 第三步
    校验指令 0002 const/4 v1, #int 0
    设置下一个指令 0003 的 insn_flags 为 Changed
    寄存器注册表 IIxL1L2

  4. 第四步
    校验指令 0003 const/16 v2, #int 24
    设置下一个指令 0005 的 insn_flags 为 Changed
    寄存器注册表 IIIL1L2

  5. 第五步
    校验分支指令 0005: if-lt v0, v2, 000e
    设置下一个指令 0007 的 insn_flags 为 Changed
    设置下个分支第一个指令 000e 的 insn_flags 为 Changed
    寄存器注册表 IIIL1L2
    复制寄存注册表到 000e 上

  6. 第六步
    校验指令 0007: invoke-virtual {v4}, Landroid/app/Activity;.isInMultiWindowMode:()Z
    检验发现 isInMultiWindowMode 方法不存在。该异常会导致出现运行期异常。 该条链路以下的指令不再校验。 不再为任何指令设置 Changed 。
    当前寄存器注册表 IIIL1L2

  7. 第七步
    由于 000e 的 insn_flags 还是 Changed。还需要校验指令 000e 指令
    校验指令 000e: move-object v0, v4
    0x00e - 0x0018 是位于 try catch 里面的指令。 try catch 里所有可能发生异常的指令。都会走到 catch 的处理逻辑中。 所以需要把进入该指令前的寄存器注册表状态转移到 0x001a 中。进入前的寄存器注册表保存在 saved_line_ 变量上。理论上 move-object 指令是不会发生异常的。 但是 api 22 存在的一个 bug 。 由于第六步的异常导致所有的指令都强制设置为会发生异常。 导致 art 错误的把一个未赋值的 saved_line_ 寄存器注册表赋值给 0x001a ,同时设置 0x001a 的 insn_flags 设置为 Changed 。
    执行指令是否会发生异常查看 dex_instruction_list.h kThrow

  8. 第八步
    检验 001a: return v1。 检验寄存器1
    由于当前寄存器注册表未赋值为 xxxxx
    校验失败。结束校验。抛出异常

异常现场复现。

0x03 总结

Bug 如何出现 ?

这个 Bug 是一套组合。

  1. 一个运行期异常。
  2. 紧跟一个 try catch 代码块
  3. try catch 第一个指令运行不会发生异常
  4. catch异常处理第一个指令是一个从寄存器读的操作。

如何解决这个 Bug ?

  1. 弃用 D8 使用 dx 来转化 Dex (历史的倒退)

  2. 弃用 release 模式的 D8 来生成 Dex(优化力度变小)

  3. 规避特定的排序。 (看天吃饭)
    节点1 去除 isInMultiWindowMode 方法调用。
    节点2 关闭强转。
    节点3 处理异常。
    节点3 return 非 0 。

  4. 对 D8 进行干预。 关闭 move-exception 指令的优化
    MoveException.java

image.png

Bug 影响范围 ?

问题存在在 api 21-22 在 api 23 被修复。
修复的 commit 如下:

  1. saved_line_ 正确被赋值
    d7f8d059 diff

  2. have_pending_runtime_throw_failure_ 状态及时重置。
    3ae8da0 diff

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342