Robust遇到的VerifyError问题解决

最近在负责热修复相关的工作,主要采用的类似Robust方案,但是修了很多bug。这里列出我昨天修复的一个比较难找的bug。欢迎对热修复及字节码插桩感兴趣的同学可以聚集到一起交流。

一、问题的出现

很早之前有人反馈打patch时遇到了VerifyError的问题,一直没时间解。
Robust官网也有人提出了issue :https://github.com/Meituan-Dianping/Robust/issues/314,但是没人解,估计robust官方已经不怎么维护了。尝试着把这个问题解了,看着非常底层,想看看是不是javaassist本身的限制导致的这个问题。

除了上面的截图相关的信息,利用泛型我自己也在本地复现出了这个问题。

二、复现代码

SecondActivity.java
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
        TextView textView = (TextView) findViewById(R.id.test_tv);
        textView.setText(getTextInfo() + "测试");
    }
  @Modify
    private  Integer getTextInfo() {
        return getValue(12348);
    }

    private    T getValue(T value){
        return value;
    }

复现代码非常简单,假设我们有一个getValue的泛型方法,然后我们利用getTextInfo方法去调用这个getValue方法。此时假设getTextInfo方法出了问题,需要修复getTextInfo方法。运行一下,崩溃了,错误堆栈。

2019-08-01 00:56:16.864 7779-7779/com.xx.robust.demo W/System.err: java.lang.VerifyError: Verifier rejected class com.xxx.xxx.patch.SecondActivityPatch: java.lang.Integer com.xxx.xxx.patch.SecondActivityPatch.getTextInfo() failed to verify: java.lang.Integer com.xxx.xxx.patch.SecondActivityPatch.getTextInfo(): [0x46] returning 'Reference: com.xxx.xxx.demo.SecondActivity', but expected from declaration 'Precise Reference: java.lang.Integer' (declaration of 'com.xxx.xxx.patch.SecondActivityPatch' appears in /data/user/0/com.xxx.robust.demo/cache/robust/patch_temp.jar)

异常的大概意思就是寄存器中的类型值错误:函数准确的返回值应该是java.lang.Integer,但是得到的返回值是com.bytedance.robust.demo.SecondActivity(originClass),乍一看非常懵逼。难道robust生成patch字节码在某些清况下有逻辑错误。

三、问题分析

1. 第一步

先看一下生成patch的class代码,反射调用getValue方法,然后是关于this的判断,为了将调用到this的地方转换成this.originClass,即调用到SecondActivityPatch的地方转换成SecondActivity对象。


Robust遇到的VerifyError问题解决_第1张图片
image.png

由于getValue这个方法编译成class后,T会被擦除为Object。对getValue反射传入的是Integer,返回值会是个Object,然后进行强转。这里看var8肯定是int,必然会走到else分支中,最终var8也一定是个Integer啊,逻辑没问题。这就奇怪了,难道是反编译工具有问题,翻译回来的java源码不准确?
接着从class字节码和smali各个层面都一行一行的把逻辑屡了一遍,把从patch.dex反编译回来的字节码也看了一遍,逻辑与上述代码完全一致,不存在问题。但是为什么校验不通过呢?模拟此代码在本地也跑了一遍,依然没发现问题。
无奈之下,看了一下android源码,抛出异常的位置,果然不出所料,啥也看不出来,只能看到a寄存器的srcType,然后得到个targetType,两个一比较不一致就抛出来了,没有任何收获。

2. 第二步

怎么办呢,突然想到会不会是静态检查太严格了,后来想了想,当拿到var8时,进行强转返回时,发现var8的类型有可能是originClass,然后就导致不通过呢,因为反射getValue方法时返回的var8是个Object,可能静态分析时判断类型无法准确获取,顺着这个思路试了试:

查看robust源码:此处应该执行的是check-cast指令
位置:com.meituan.robust.autopatch.PatchesFactory->createPatchClass

  @Override
    void edit(Cast c) throws CannotCompileException {
        MethodInfo thisMethod = ReflectUtils.readField(c, "thisMethod");
        CtClass thisClass = ReflectUtils.readField(c, "thisClass");

        def isStatic = ReflectUtils.isStatic(thisMethod.getAccessFlags());
        if (!isStatic) {
            //inner class in the patched class ,not all inner class
            if (Config.newlyAddedClassNameList.contains(thisClass.getName()) || Config.noNeedReflectClassSet.contains(thisClass.getName())) {
                return;
            }
            // static函数是没有this指令的,直接会报错。
            c.replace(ReflectUtils.getCastString(c, temPatchClass))
        }
    }

位置:com.meituan.robust.autopatch.ReflectUtils->getCastString

   def
    static String getCastString(Cast c, CtClass patchClass) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("{");
        stringBuilder.append(" if(\$1 == this ){");
        stringBuilder.append("\$_=((" + patchClass.getName() + ")\$1)." + Constants.ORIGINCLASS + ";")
        stringBuilder.append("}else{");
        stringBuilder.append("\$_=(\$r)\$1;");
        stringBuilder.append("}");
        stringBuilder.append("}");
    }

此处我们加一个 instanceof判断,增加识别能力看看。


Robust遇到的VerifyError问题解决_第2张图片
image.png

发现依然报错,但是报错的信息有些改变:貌似不会傻傻的判断成originClass,此时判断成Object了,虽然依然有问题,但是基本上确定确实是因为这块if-else写法导致的。


image.png
3. 第三步

既然问题是发生在return语句,我们能否针对return语句做一些操作,不进行上述的强转,后来发现不行,因为javaassist提供的api无法识别return语句,这点和ASM比确实弱爆了,这也是为什么Robust不支持 return this指令,因为它拿不到这行信息。那我们就换一种等价的写法试试,首先Check-cast指令主要是为了处理 this强转问题(其实出现使用this强转的请况几乎没有,很少人会这么写。。。,但是它既然存在了,就在它基础上修改吧)。

看一下这段javaassist代码:

修改前:

       def
    static String getCastString(Cast c, CtClass patchClass) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("{");
        stringBuilder.append(" if(\$1 == this ){");
        stringBuilder.append("\$_=((" + patchClass.getName() + ")\$1)." + Constants.ORIGINCLASS + ";")
        stringBuilder.append("}else{");
        stringBuilder.append("\$_=(\$r)\$1;");
        stringBuilder.append("}");
        stringBuilder.append("}");
    }
image.png

修改后:

   static String getCastString(Cast c, CtClass patchClass) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("{");
        stringBuilder.append(" if(\$1 == this ){");
        stringBuilder.append("\$1=this." + Constants.ORIGINCLASS + ";")
        stringBuilder.append("}");
        stringBuilder.append("\$_=(\$r)\$1;");
        stringBuilder.append("}");
    }
 if(var3==this){
      var3=this.originClass;
 }
  var12=(Integer)var3;
  return var12;

这么点改动,就把问题解决了,不信你可以尝试。
其实修改前逻辑是有问题的,比如参数值是this时 他只是给转换成了this.originClass,但是并没有执行
$_=($r)$1;强转语句,这样下面用到的时候是有一定几率出现方法找不到的问题的,而修改后的方式,如果为this,则替换成this.originClass,然后依然执行强转指令,因为按照源码逻辑 this.originclass和强转后的类型必然是兼容的,否则源码根本就编译不过去,退一步讲,如果是真的不兼容了,说明逻辑肯定是有问题的,就应该报错,而修改前的方式则出问题的几率更大。

综上,修改后有两个好处:

  • 解决了VerifyError问题,因为无论if这么判断,都会执行强转,那么虚拟机校验时是可以通过的,至于运行时有没有问题,就不是虚拟机校验的事了。

  • 安全性更高,不容易引起潜在问题。

四、收获

  • 热修就是踩坑的过程,永远踩不完!

  • javaassist能力真的有限,感受到灵活度不够且生成的字节码冗余度比较大,很多莫名其妙的局部变量,但是操作起来是真的效率高,简单的要命。

此外,我建了一个robust热修复相关的群,欢迎感兴趣的同学进群讨论。


Robust遇到的VerifyError问题解决_第3张图片
image.png

你可能感兴趣的:(Robust遇到的VerifyError问题解决)