最近在负责热修复相关的工作,主要采用的类似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对象。
由于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判断,增加识别能力看看。
发现依然报错,但是报错的信息有些改变:貌似不会傻傻的判断成originClass,此时判断成Object了,虽然依然有问题,但是基本上确定确实是因为这块if-else写法导致的。
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("}");
}
修改后:
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能力真的有限,感受到灵活度不够且生成的字节码冗余度比较大