ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

ASM 字节码调试信息全解析:visitLineNumber、visitLocalVariable 与 SKIP_DEBUG 实战

ASM 字节码调试信息全解析:visitLineNumber、visitLocalVariable 与 SKIP_DEBUG 实战 文档教程后端【免费下载链接】CodeGuide:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总旨在为大家提供一个清晰详细的学习教程侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助请给予支持(关注、点赞、分享)项目地址https://gitcode.com/gh_mirrors/code/CodeGuide点击查看免费下载以javac -g编译的类文件中除了字节码指令本身还隐藏着源文件名、源代码行号与字节码指令的映射、以及局部变量名与局部变量槽的映射三部分调试信息——它们正是调试器、异常栈轨迹、覆盖率工具和各类字节码插桩框架赖以工作的基础。本文以 CodeGuide 仓库中《4.3 调试》为核心结合 ASM 核心 API 的visitSource、visitLineNumber、visitLocalVariable三个方法从类文件结构、事件访问顺序、SKIP_DEBUG读取选项到工具链TraceClassVisitor / CheckClassAdapter / ASMifier与树 API 视角完整讲解如何在生成与转换类时正确读写调试信息读完即可在自己的字节码增强项目中落地。一、调试信息在类文件中的存放结构调试信息是可选信息只有当使用javac -g编译类时编译产物中才会包含它。此时类文件中会额外携带三部分内容类对应的源文件名源代码行号与字节码指令之间的映射源代码中局部变量名与字节码中局部变量槽之间的映射。当这些信息可用时调试器和异常栈轨迹会使用它们。在 Java 虚拟机规范中它们分别属于类文件的不同结构区域。1.1 源文件名独立的类文件结构部分一个类的源文件名存储在一个专门的类文件结构部分中。正如 2.1 结构 中给出的已编译类整体结构图 2.1所示源文件名位于类的修饰符、名字、超类、接口与常量池之后、字段与方法之前是一个可选条目。这就是为什么你在 IDE 中打开一个只含.class文件的反编译视图时往往还能看到它来自哪个.java文件。1.2 行号映射由line number, label对组成的列表源代码行号与字节码指令之间的映射以行号, 标记对组成的列表形式存储在该方法的已编译代码部分中。例如如果l1、l2和l3是按此顺序出现的三个标记那么下面各对(n1, l1) (n2, l2) (n3, l3)意味着l1和l2之间的指令来自源代码行n1l2和l3之间的指令来自行n2l3之后的指令来自行n3。这里有一个关键细节一个给定行号可以出现在几个对中。原因在于源代码中位于同一行的表达式其编译后的字节码指令可能并不连续。最典型的例子是for循环for (init; cond; incr) statement;它通常按如下顺序编译init statement incr cond也就是说cond条件判断的字节码在物理位置上反而落在循环体statement和增量incr之后但它们在源代码中可能同属for那一行因此该行号会在多个位置出现。1.3 局部变量映射五元组列表源代码中局部变量名与字节码中局部变量槽之间的映射以name, type descriptor, type signature, start, end, index五元组列表的形式存储在该方法的已编译代码部分中。一个五元组的含义是在两个标记start和end之间槽index中的局部变量对应于源代码中名字和类型由前三个元素指定的局部变量。理解这个五元组时有两点极易混淆编译器可以使用相同的局部变量槽来存储不同作用范围的源局部变量同一个源代码局部变量也可能被编译为具有非连续作用范围的局部变量槽。例如可能存在类似下面的情景——槽 1 在方法的不同阶段分别保存了i、j、il1: ... // 这里的槽 1 包含局部变量i l2: ... // 这里的槽 1 包含局部变量j l3: ... // 这里的槽 1 再次包含局部变量i end:相应的五元组为(i, I, null, l1, l2, 1) (j, I, null, l2, l3, 1) (i, I, null, l3, end, 1)其中I是int的类型描述符类型描述符的完整规则可参见 2.1 结构基元类型int对应Inull表示泛型签名1是局部变量槽索引。二、核心 APIvisitSource / visitLineNumber / visitLocalVariable调试信息通过ClassVisitor和MethodVisitor类的三个方法访问调试信息访问方法所属类源文件名visitSourceClassVisitor源代码行号与字节码指令的映射visitLineNumber每次访问一对MethodVisitor局部变量名与局部变量槽的映射visitLocalVariable每次访问一个五元组MethodVisitor完整的MethodVisitor方法签名如下出自 3.2 接口和组件void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index); void visitLineNumber(int line, Label start);可以看到visitLocalVariable的参数与五元组一一对应name是局部变量名、desc是类型描述符、signature是泛型签名无则为null、start/end是作用范围起止标记、index是局部变量槽索引。visitLineNumber则接收行号line和该行起始指令对应的标记start。2.1 调用顺序约束紧跟标记之后visitLineNumber方法必须在已经访问了作为参数传送的标记之后进行调用。在实践中就是在访问这一标记后立即调用它从而可以非常容易地知道一个方法访问器中当前指令的源代码行。下面的MyAdapter通过重写visitLineNumber在转发事件的同时记录当前行号实现知道每条指令来自源代码哪一行的能力public class MyAdapter extends MethodVisitor { int currentLine; public MyAdapter(MethodVisitor mv) { super(ASM4, mv); } Override public void visitLineNumber(int line, Label start) { mv.visitLineNumber(line, start); currentLine line; } ... }类似地visitLocalVariable方法也必须在已经访问了作为参数传送的标记之后调用。这是因为局部变量的作用范围由标记对start/end界定ASM 的访问器只有先见过这些标记才能将五元组与正确的指令位置关联起来。2.2 完整调用示例下面给出一些方法调用示例它们与第 1.2、1.3 节给出的名称值对和五元组一一对应visitLineNumber(n1, l1); visitLineNumber(n2, l2); visitLineNumber(n3, l3); visitLocalVariable(i, I, null, l1, l2, 1); visitLocalVariable(j, I, null, l2, l3, 1); visitLocalVariable(i, I, null, l3, end, 1);结合 3.2 接口和组件 中给出的MethodVisitor方法调用顺序约束调试信息相关调用应出现在方法代码访问的中段即visitCode()与visitMaxs()之间visitAnnotationDefault? ( visitAnnotation | visitParameterAnnotation | visitAttribute )* ( visitCode ( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn | visitLocalVariable | visitLineNumber )* visitMaxs )? visitEnd这也解释了为什么在实现有状态转换如删除指令序列时标记和帧会影响匹配结果而行号信息不会行号虽然与指令同时被访问但一条指令序列中间出现行号并不会影响该序列能否被删除或转换详见 3.2 接口和组件 3.2.5 节。因此模式匹配类适配器如PatternMethodAdapter通常会重写visitFrame和visitLabel参与匹配但对visitLineNumber直接忽略。三、忽略调试信息SKIP_DEBUG 选项及其它读取选项3.1 为什么需要 SKIP_DEBUG为了访问行号和局部变量名ClassReader类可能需要引入人为的Label对象——也就是说跳转指令并不需要这些标记它们只是为了表示调试信息而创建的。这可能会在 3.2 接口和组件 3.2.5 节介绍的情景中导致错误判断在该情景中指令序列中部的一个Label会被认为是一个跳转目标因此禁止这一序列被删除。例如在实现删除ICONST_0 IADD加 0 无实际效果这类序列删除优化时如果行号映射在两个指令之间引入了人为标记优化就可能被意外阻断。为避免这种误判可以在ClassReader.accept方法中使用SKIP_DEBUG选项ClassReader cr new ClassReader(className); cr.accept(cv, ClassReader.SKIP_DEBUG);有了这一选项类读取器不会访问调试信息也不会为它创建人为标记。当然代价是调试信息会从类中删除因此只有在不会为应用程序造成问题时才能使用这一选项——例如做运行时性能插桩、对不需要调试的类做转换时是安全的但如果转换后的类还要被调试器使用或异常栈轨迹需要展示源代码行号就不能随意丢弃。3.2 其它读取选项ClassReader类还提供了其他选项可按需组合使用注意选项之间用|按位或连接选项作用SKIP_DEBUG跳过调试信息源文件、行号、局部变量的访问不创建人为标记SKIP_CODE跳过对已编译代码的访问如果只需要类的结构修饰符、名字、字段、方法签名这个选项很有用SKIP_FRAMES跳过栈映射帧EXPAND_FRAMES解压缩这些帧与SKIP_FRAMES相反在 CodeGuide 仓库的 ASM 实战案例中可以看到这些选项的典型用法。例如 【ASM 字节码编程】用字节码增强技术给所有方法加上 TryCatch 捕获异常并输出 中通过类名构造ClassReader并调用cr.accept(cv, ClassReader.EXPAND_FRAMES)在 【ASM 字节码编程】JavaAgentASM 字节码插桩采集方法名称以及入参和出参结果并记录方法耗时 中则是基于 JavaAgent 传入的classfileBuffer构造ClassReader再同样以EXPAND_FRAMES展开帧。这是 ASM 处理字节码增强的标准模板先创建ClassReader读取类支持类名、字节数组、输入流等多种构造方式再创建ClassWriter用于写出最后cr.accept(cv, flags)驱动整个访问链。四、工具TraceClassVisitor / CheckClassAdapter / ASMifier 观察调试信息和泛型与注释的情景一样可以使用TraceClassVisitor、CheckClassAdapter和ASMifier三个工具类来了解如何使用调试信息。它们都位于org.objectweb.asm.util包中仅在开发类生成器或适配器时使用运行时不需要详见 2.3 工具。4.1 TraceClassVisitor查看调试信息的文本表示TraceClassVisitor扩展自ClassVisitor生成所访问类的文本表示同时可以将其方法的所有调用委托给另一个访问器如ClassWriterClassWriter cw new ClassWriter(0); TraceClassVisitor cv new TraceClassVisitor(cw, printWriter); cv.visit(...); ... cv.visitEnd(); byte b[] cw.toByteArray();在输出中调试信息以注释形式呈现例如// class version 49.0 (49)、// access flags ...、// signature ...、// declaration: ...等行都来自调试信息。想确认转换后类里还残留哪些行号、局部变量信息这是最直接的观察手段。也可以在生成链或转换链的任意位置插入TraceClassVisitor查看链中这一点的状态由于它输出的文本可比较还可以用String.equals()对两个类做文本级对比。4.2 CheckClassAdapter校验访问顺序与参数ClassWriter不会核实对其方法的调用顺序是否恰当、参数是否有效因此有可能生成被 Java 虚拟机验证器拒绝的无效类。CheckClassAdapter用于在生成阶段尽早发现这类错误它校验调用顺序与参数有效性后再委托给下一个访问器出错时抛出IllegalStateException或IllegalArgumentException。校验、打印、产出字节数组三合一的链式写法ClassWriter cw new ClassWriter(0); TraceClassVisitor tcv new TraceClassVisitor(cw, printWriter); CheckClassAdapter cv new CheckClassAdapter(tcv); cv.visit(...); ... cv.visitEnd(); byte b[] cw.toByteArray();注意访问器链的顺序决定操作顺序上述代码先核对再打印若将CheckClassAdapter包在TraceClassVisitor里则是先打印再核对。类似的方法级校验可以使用CheckMethodAdapter见 3.3 工具。4.3 ASMifier反向生成 ASM 代码ASMifier为TraceClassVisitor提供另一种后端默认的Textifier后端打印类的文本表示而ASMifier后端则让每个方法打印调用它的 Java 代码。也就是说当使用ASMifier后端的TraceClassVisitor访问一个类时会输出用 ASM API 生成这个类的源代码。如果你不知道如何用 ASM 生成某个包含调试信息的已编译类一个高效做法是用 Java 编写相应的源代码用javac -g编译它再用ASMifier访问编译后的类就会得到包含visitSource、visitLineNumber、visitLocalVariable调用的完整 ASM 生成代码。ASMifier也可以直接在命令行中使用java -classpath asm.jar:asm-util.jar \ org.objectweb.asm.util.ASMifier \ java.lang.Runnable五、树 API 视角LineNumberNode 与 LocalVariableNode除了事件驱动的核心 APIASM 还提供了基于树的 APIorg.objectweb.asm.tree包其调试信息的承载方式在 9.3 调试 中有明确说明作为被编译类来源的源文件存储在ClassNode的sourceFile字段中关于源代码行号的信息存储在LineNumberNode对象中它继承自AbstractInsnNode。与核心 API 中行号信息与指令同时被访问一样在树 API 中LineNumberNode对象是指令列表的一部分——这正是行号不参与指令序列匹配这一结论在树结构上的体现源局部变量的名字和类型存储在MethodNode的localVariables字段中它是LocalVariableNode对象的一个列表每个节点对应核心 API 中的一个五元组。因此在做指令级分析如基于InsnList遍历做死代码消除、序列匹配时务必把LineNumberNode当作指令来处理或显式忽略避免因调试节点打断指令遍历逻辑。六、实战要点小结围绕调试信息编写 ASM 类生成与转换代码时有以下几点值得记住调试信息是可选资产只有javac -g编译的类才携带源文件名、行号映射与局部变量映射visitSource、visitLineNumber、visitLocalVariable三个方法分别读取它们。顺序敏感visitLineNumber与visitLocalVariable必须在作为参数传入的标记被访问之后调用在MethodVisitor的事件序列中它们与visitTryCatchBlock、visitLabel、visitFrame、visitXxxInsn同处visitCode()与visitMaxs()之间。行号映射不连续同一源代码行可能对应多段不相邻的字节码如for循环的init statement incr cond一个行号可以出现在多个对中同一局部变量槽也可以在不同作用范围承载不同名字的局部变量。SKIP_DEBUG是双刃剑它可以避免ClassReader为调试信息创建人为标记、防止序列删除类转换被误判但代价是调试信息从类中消失配合SKIP_CODE、SKIP_FRAMES、EXPAND_FRAMES可按需控制读取粒度。调试工具链完备TraceClassVisitor看文本、CheckClassAdapter查顺序与参数、ASMifier反推生成代码树 API 侧则以ClassNode.sourceFile、LineNumberNode、MethodNode.localVariables承载同等信息。理解了调试信息在类文件中的结构与访问方式你就能在自己的字节码增强、方法耗时采集、代码覆盖率统计乃至反混淆工具中正确地保留、更新或按需丢弃这些信息让插桩后的代码既能在调试器中正常单步又不会因人为标记干扰指令级优化。赞分享文档教程后端【免费下载链接】CodeGuide:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总旨在为大家提供一个清晰详细的学习教程侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助请给予支持(关注、点赞、分享)项目地址https://gitcode.com/gh_mirrors/code/CodeGuide点击查看免费下载相关推荐Java字节码操作终极指南ASM与Javassist实战解析Java字节码操作终极指南ASM与Javassist实战解析 Java字节码操作是高级开发中的关键技术它允许开发者直接操控Java类文件的字节码实现诸如动文档教程后端终极指南揭秘Java字节码编程ASM与Javassist实战技巧终极指南揭秘Java字节码编程ASM与Javassist实战技巧 Java字节码编程是高级Java开发者必须掌握的核心技能之一而ASM和Javassist正文档教程后端Java字节码操作终极指南ASM、Javassist与Byte Buddy实战解析Java字节码操作终极指南ASM、Javassist与Byte Buddy实战解析 Java字节码操作是Java开发中一个强大而神秘的技术领域它允许开发者在示例工程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表