妖怪般的VerifyError | 奇形怪状的bug

前言

任何的Transform的字节都是危险的,所以如果真的觉得自己不能解决所有线上奇奇怪怪的问题,对这门技术还是要慎重。 出自很菜的虾。

如果你是因为这个bug,不幸点入这篇文章,我想说你运气属实不好,那么让我们掌声欢迎这个受害者。

首先我个人觉得这个问题非常难排查和定位,光从堆栈日志来说,你可能会一头雾水。

VerifyError问题排查

首先先看下这个异常的定义。

java.lang.VerifyError 是说 JVM 在加载一个类时,会去校验类的正确性,只有类文件不合法才会报这个Error,这个异常发生在类的加载过程中。

这个问题发生在类的生命周期的过程中。大体上还是和之前我写的那篇文章一个一年没解决的ClassNotFoundException|类加载机制探索这个问题非常类似。这个也是来b之前和字节大佬面试切磋时的一个问题吧,现在回头看看,其实还是受益匪浅,也大概知道这种问题如何去查看和调试了。

问题的本质还是之前说的androidx的升级。因为升级了fragment的版本到1.4.3,对于基础的fragment构造增加了一个有参的构造函数,之后导致了这个奇怪的问题。

在线上灰度的期间,我们发现了4.4的设备有这么个奇怪的crash异常。而在别的系统的设备则都是正常的。之后我们找了一台4.4的设备,发现在release版本的确会出现这个问题,异常情况如下所示。

FATAL EXCEPTION: main
at java.lang.VerifyError: androidx/fragment/app/DialogFragment
at androidx.fragment.app.Fragment.performCreate(Fragment.java:2949)
at androidx.fragment.app.FragmentStateManager.create(FragmentStateManager.java:475)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:278)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2189)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2100)
at androidx.fragment.app.FragmentManager.execSingleAction(FragmentManager.java:1971)
at androidx.fragment.app.BackStackRecord.commitNowAllowingStateLoss(BackStackRecord.java:311)
at androidx.fragment.app.FragmentPagerAdapter.finishUpdate(FragmentPagerAdapter.java:249)
at androidx.viewpager.widget.ViewPager.populate(ViewPager.java:1244)
at androidx.viewpager.widget.ViewPager.setCurrentItemInternal(ViewPager.java:669)
at androidx.viewpager.widget.ViewPager.setCurrentItemInternal(ViewPager.java:631)
at androidx.viewpager.widget.ViewPager.dataSetChanged(ViewPager.java:1086)
at androidx.viewpager.widget.ViewPager$PagerObserver.onChanged(ViewPager.java:3097)
at androidx.viewpager.widget.PagerAdapter.notifyDataSetChanged(PagerAdapter.java:291)
at android.os.Handler.handleCallback(Handler.java:730)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:5162)
at java.lang.reflect.Method.invokeNative(Method.java)
at java.lang.reflect.Method.invoke(Method.java:525)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:737)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
at dalvik.system.NativeStart.main(NativeStart.java)

现象很简单,这次我们所有的dialogfragment其实全挂了,在4.4的设备上,只要有这个的构造就会导致这个异常崩溃。

起初我们只是以为是混淆导致的这个异常情况的发生,但是尝试keep了所有androidx的类之后,发现这个问题还是稳定的复现,我有点懵逼了。

而之后简单的分析了下异常,猜测dialogfragment的类本身就出现了问题,所以导致了在类加载的时候,验证字节码安全性失败。但是这部分可是androidx内部的代码啊,这不就是不讲武德吗?

当自己走进死胡同的时候,还是可以尝试问下你周围的同事,也可以是你的大佬,正所谓一人计短。有时候的确是会有奇效的,也算是一个老司机理论了。

我大佬之前的确处理过类似的问题,但是他之前处理的那部分是所有机型都必挂,而且是和混淆相关。但是大佬的大佬也说了下,这种异常在崩溃日志之前就会有类信息校验的错误说明,也就是说dalvikvm在校验代码的时候会把错误的信息打印出来。恰巧就和之前的classnotfound异常对应上了。

06-15 18:11:16.876 13235-13235/? W/dalvikvm: VFY: invoke-direct <init> must be on current class or super
06-15 18:11:16.876 13235-13235/? W/dalvikvm: VFY:  rejecting opcode 0x70 at 0x0000
06-15 18:11:16.876 13235-13235/? W/dalvikvm: VFY:  rejected Landroidx/fragment/app/DialogFragment;.<init> (I)V

从描述上来看,就是构造函数内调用的是当前类的,并没有调用父类的构造。所以导致了这部分是一个异常的字节码。

因为这部分我司做了一部分字节码的父类替换,所以DialogFragment其实已经被修改了一部分了。而在新版本的androidx中,则给Fragment添加了另外一个有参的构造函数,所以这部分就出现了异常。

西内,无能狂怒,问题定位出来之后后续的其实也就相对来说还好了,改造方式则是有另外一个大佬去完成的,有兴趣可以参考下大佬之前写的lancet,功能也比较相似,细节我说出来可能要去趟hr办公室领离职证明了。

借住AS查看Apk的bytecode

另一个大佬顺便教会了我如何从apk中去查看最终产物的bytecode的方式。一个不需要用jadx反编译整个apk就可以快速查看class字节码的方式。

通过这部分就可以快速的查看这部分异常了,方式就和下面我所截图的一样了。

  1. 拖入android studio,点击apk

妖怪般的VerifyError | 奇形怪状的bug

  1. 找到你想看的类,右键 show bytecode

妖怪般的VerifyError | 奇形怪状的bug

  1. 看一看

妖怪般的VerifyError | 奇形怪状的bug

这部分异常参考资料

Android 不想和你说话,抛了个 java.lang.VerifyError 这个是摘自另外一个哔哩哔哩的安卓巨佬了。

因为是在低版本手机上触发的问题,运行的仍然是 dalvik VM,很容易的(google)在对应版本(4.1.1)源码中找到类DexVerify.cpp,和 CodeVerify.cpp

(感兴趣的可以从 dvmVerifyClass() 开始阅读类检查的全过程。)

DexVerify 中的 verifyMethod() 最终会调用 CodeVerify 的 dvmVerifyCodeFlow() 来确保类中的单个方法执行流是合法的。

其中要注意的是,异常处理(Exception Hanler)也是在这个时候被校验的,它的opcode是OP_MOVE_EXCEPTION(0x0d,就是前面日志”rejecting opcode 0x0d”提到的)。

检验方法getCaughtExceptionType() 在找不到catch代码块中指定的异常类(如例子中的ErrnoException)时即会报错:”VFY: unable to resolve exception class 1594 (Landroid/system/ErrnoException;)”,尝试各种可能性之后仍然不知道该如何处理这个异常,接着会认为代码有问题日志报错:”VFY: unable to find exception handler at addr 0xe” 和 “VFY: rejected Lcom/sample/FileUtils;.getUid (Ljava/lang/String;)I”

最终走向方法校验失败的分支”rejecting opcode 0x0d at 0x000e”,于是乎dvmVerifyCodeFlow()方法return false标识着verifyMethod()失败,拒绝加载类:”Verifier rejected class Lcom/sample/FileUtils;”

总结

我个人觉得哦,一个开发同学,其实多和别人交流交流,对于你的技术提升啊问题解决,还有很多思路相关的其实都是有很大帮助的。

也不一定是要学到什么自己完全不会的东西,可能一些思路啊,解决问题的方式都是值得你借鉴学习的。而且去向别的同事学习,也可以满足下他们装杯的欲望,他们说实话真的是不会拒绝你的。

这次文章相对来说很短,但是无奈与作者水平有限啊,只能打这么多字了,多有得罪,你特么也打不到我啊!!!!

还有最后说三句,算上这句,没有了。

嗯,下面给大家推荐一个我的Android开发社区,大佬云集,小白闻着味都能起飞。

加群地址:点击直达

上一篇:使用 Material Design 组件实现 Material 动效


下一篇:4-2-1 Fragment(一)