ASM修改Scala3类时未改动方法报栈映射帧不一致VerifyError
问题背景
- 编写了一段字节码处理程序:通过
ClassReader读入目标类,创建ClassVisitor为类新增两个成员(一个构造方法、一个普通方法),最终通过ClassWriter输出修改后的类文件。 - 运行修改后的类时,Java类加载器在一个原有未被修改的方法末尾的
ARETURN指令处抛出VerifyError,报错信息为分支目标位置栈映射帧(stackmap frames)不一致。 - 目标类由Scala 3(Dotty)编译生成,编译器直接输出的原始类可正常运行;触发报错的是Scala case class自动生成的
productElement方法,和新增的方法没有关系。 - 目前为
ClassWriter配置了ClassWriter.COMPUTE_FRAMES参数,不确定是否应当改用COMPUTE_INSERTED_FRAMES参数让ASM仅调整新增方法的栈帧,也怀疑自己对COMPUTE_INSERTED_FRAMES的作用理解有误——该常量的Javadoc说明比较模糊。 - 测试发现将
ClassWriter的参数从COMPUTE_FRAMES改为0后,问题就会消失。
现象对比
对比修改前后的类文件,提取这个被直接复制、却触发报错的方法,发现方法其余代码经Diff对比无任何改动,仅最后一个栈帧条目存在细微差异:
- Scala编译器生成的原始方法最后一个栈帧条目:
frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class uk/co/dga/openbusiness/ubl/Code, int, int ] stack = [ class java/lang/Object ]
- 经ASM复制处理后的对应栈帧条目:
frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class uk/co/dga/openbusiness/ubl/Code, int, int ] stack = [ class scala/Option ]
具体抛出的错误信息如下:
Exception in thread "main" java.lang.VerifyError: Inconsistent stackmap frames at branch target 175 Exception Details: Location: uk/co/dga/openbusiness/ubl/Code.productElement(I)Ljava/lang/Object; @175: areturn Reason: Type 'java/lang/String' (current frame, stack[0]) is not assignable to 'scala/Option' (stack map, stack[0]) Current Frame: bci: @60 flags: { } locals: { 'uk/co/dga/openbusiness/ubl/Code', integer, integer } stack: { 'java/lang/String' } Stackmap Frame: bci: @175 flags: { } locals: { 'uk/co/dga/openbusiness/ubl/Code', integer, integer } stack: { 'scala/Option' } Bytecode: 0000000: 1b3d 1caa 0000 0099 0000 0000 0000 0009 0000010: 0000 0035 0000 003f 0000 0049 0000 0053 0000020: 0000 005d 0000 0067 0000 0071 0000 007b 0000030: 0000 0085 0000 008f 2ab6 012a a700 7300 0000040: 00bf 2ab6 012d a700 6900 00bf 2ab6 0130 0000050: a700 5f00 00bf 2ab6 0133 a700 5500 00bf 0000060: 2ab6 0136 a700 4b00 00bf 2ab6 0139 a700 0000070: 4100 00bf 2ab6 013c a700 3700 00bf 2ab6 0000080: 013f a700 2d00 00bf 2ab6 0142 a700 2300 0000090: 00bf 2ab6 0145 a700 1900 00bf bb01 4759 00000a0: 1bb8 014d b601 51b7 0154 bf00 00bf bfb0 00000b0: Stackmap Table: append_frame(@56,Integer) full_frame(@63,{},{Object[#343]}) append_frame(@66,Object[#2],Integer,Integer) full_frame(@73,{},{Object[#343]}) append_frame(@76,Object[#2],Integer,Integer) full_frame(@83,{},{Object[#343]}) append_frame(@86,Object[#2],Integer,Integer) full_frame(@93,{},{Object[#343]}) append_frame(@96,Object[#2],Integer,Integer) full_frame(@103,{},{Object[#343]}) append_frame(@106,Object[#2],Integer,Integer) full_frame(@113,{},{Object[#343]}) append_frame(@116,Object[#2],Integer,Integer) full_frame(@123,{},{Object[#343]}) append_frame(@126,Object[#2],Integer,Integer) full_frame(@133,{},{Object[#343]}) append_frame(@136,Object[#2],Integer,Integer) full_frame(@143,{},{Object[#343]}) append_frame(@146,Object[#2],Integer,Integer) full_frame(@153,{},{Object[#343]}) append_frame(@156,Object[#2],Integer,Integer) full_frame(@171,{},{Object[#343]}) same_locals_1_stack_item_frame(@174,Object[#343]) full_frame(@175,{Object[#2],Integer,Integer},{Object[#282]}) at uk.co.dga.openbusiness.ui.App.<init>(App.scala:41) at uk.co.dga.openbusiness.ui.App$.main(App.scala:123) at uk.co.dga.openbusiness.ui.App.main(App.scala)
问题根因
COMPUTE_FRAMES的逻辑非常直接:只要开启这个参数,ASM会完全丢弃类中所有已存在的栈映射帧,不管你有没有修改原有方法,都会从每个方法的第一条指令开始,从头到尾为所有方法重新推导计算全部栈帧。- ASM自带的类型推导逻辑比较简单,默认的公共父类计算逻辑不会加载实际的类做完整的继承关系校验,遇到多分支返回不同子类型的场景时,很容易推导错误。你遇到的场景里,
productElement方法的多个分支分别返回String、Option等不同类型,这些类型都符合方法声明的Object返回值要求,但ASM重新计算栈帧时,错误地将分支汇合点的栈顶类型推导成了某一个分支的返回类型scala.Option,而不是正确的公共父类java/lang/Object,最终导致字节码校验时发现栈上实际的String类型和栈帧记录的Option不匹配,抛出VerifyError。 - 你对
COMPUTE_INSERTED_FRAMES的理解确实存在偏差:这个参数的作用不是“仅调整新增方法的栈帧”,而是当你在已有方法中插入新的字节码指令时,让ASM自动为你插入的代码片段补全对应的栈帧,原有方法中已经存在的栈帧会被保留,不会被全局重算。如果完全不修改已有方法的字节码、只是新增方法,这个参数本身不会触发任何栈帧重算逻辑。
正确配置方案
- 如果只是新增类成员/方法,完全不修改原有方法的字节码:直接给
ClassWriter传参数0即可。此时ASM会原样保留原有方法的所有栈帧,不会做全局重算,你只需要为自己新增的方法手动写入正确的栈映射帧,或者单独为新增方法做栈帧计算即可,这也是你把参数改成0后问题消失的原因。 - 如果需要修改已有方法的字节码、插入新指令:使用
ClassWriter.COMPUTE_FRAMES参数的同时,必须重写ClassWriter的getCommonSuperClass方法,用当前业务环境的类加载器实际加载类、计算两个类型的真实公共父类,替换ASM默认的简陋实现,避免类型推导错误。如果只是在已有方法里插入零散指令、不改动原有控制流,可以搭配COMPUTE_INSERTED_FRAMES参数,减少全量重算栈帧带来的错误风险。 - 注意不要单独使用
COMPUTE_INSERTED_FRAMES,这个参数本身不触发栈帧计算逻辑,必须和栈帧计算相关参数配合才能生效。
内容的提问来源于stack exchange,提问作者David Goodenough
相关产品推荐
相关产品推荐

