You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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对比无任何改动,仅最后一个栈帧条目存在细微差异:

  1. Scala编译器生成的原始方法最后一个栈帧条目:
frame_type = 255 /* full_frame */
offset_delta = 0
locals = [ class uk/co/dga/openbusiness/ubl/Code, int, int ]
stack = [ class java/lang/Object ]
  1. 经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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 00:57:32