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> 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对象。


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判断,增加识别能力看看。


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能力真的有限,感受到灵活度不够且生成的字节码冗余度比较大

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

推荐阅读更多精彩内容