java.lang.VerifyError:一技组合拳

最近整体升级了项目的工具链。 使用了 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 字节码的异同。

可以看出方法使用的寄存器 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

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章