文档教程后端【免费下载链接】CodeGuide:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总旨在为大家提供一个清晰详细的学习教程侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助请给予支持(关注、点赞、分享)项目地址https://gitcode.com/gh_mirrors/code/CodeGuide点击查看免费下载导读本篇基于 ASM 方法分析 APIorg.objectweb.asm.tree.analysis包展开讲解如何借助一套可复用的正向数据流分析框架对已加载到内存中的MethodNode进行死代码移除、字节码合法性验证、冗余类型转换消除、潜在空指针检测以及圈复杂度统计。读完本文你将掌握Analyzer、Frame、Interpreter、Value四类核心角色的分工能够基于预定义的BasicInterpreter、BasicVerifier、SimpleVerifier快速上手也能通过自定义Interpreter子类实现属于自己的数据流分析并将控制流图用于复杂度度量。相关上下文可先阅读 8.0方法分析.md 与 8.1介绍.md 了解数据流/控制流分析的基本概念。一、框架总体设计固定的算法骨架与可变的语义解释用于代码分析的 ASM API 位于org.objectweb.asm.tree.analysis包中。从包名即可看出它建立在树 API 之上方法体以MethodNode.instructions指令链表形式存在见 7.1接口和组件.md并提供了一个进行正向数据流分析的框架。数据流分析见 8.1介绍.md的目的是对方法中所有可能出现的参数值模拟所有可能执行路径而不是像 JVM 解释器那样只沿着由一组具体参数决定的单一路径执行。其结果是分支指令的两条分支都会被模拟所处理的值实际上是由可能取值组成的集合如所有整数所有 String 对象所有非 null 对象并且需要计算这些取值集合的并集。为了让准确度不一的取值都能进行数据流分析框架刻意将算法拆成两部分固定部分框架提供整体数据流分析算法、以及将适当数量的值从操作数栈弹出/压回的任务只实现一次供Analyzer和Frame类使用。也就是说指令执行时栈帧如何推进、控制流如何遍历都由框架保证。可变部分用户提供合并值、计算取值集合并集等语义任务由用户定义的Interpreter和Value抽象类子类提供。框架针对不同精度BasicValue的 7 个值集、按类区分的值、{null, 非null, 可为null}三分法……预定义了若干子类后续各节逐一介绍。此外尽管框架的主要目的是数据流分析Analyzer类同样可用于构造所分析方法的控制流图只需重写newControlFlowEdge和newControlFlowExceptionEdge两个方法默认实现不做任何事情其结果即可用于控制流分析如后文的圈复杂度计算。二、BasicInterpreter 与七个预定义值集BasicInterpreter是框架预定义的一个基本Interpreter子类它利用BasicValue类中定义的七个值集来模拟字节码指令的效果覆盖了 JVM 操作数栈上所有可能的值形态值集含义UNINITIALIZED_VALUE指所有可能值通常表示值尚未被初始化INT_VALUE指所有 int、short、byte、boolean 或 char 值FLOAT_VALUE指所有 float 值LONG_VALUE指所有 long 值DOUBLE_VALUE指所有 double 值REFERENCE_VALUE指所有对象和数组值RETURNADDRESS_VALUE用于子例程见 A.2子例程.md需要指出的是BasicInterpreter本身并不是非常有用——方法帧Frame中已经提供了相同且更详细的信息——但它可以充当一个空的Interpreter实现来构建Analyzer成为许多自定义分析器的起点本文第四、五、六、七、八节全部以它或其子类为基座。三、基本数据流分析用 Analyzer 检测并移除死代码即使是最基础的AnalyzerBasicValue也立刻能派上一个实用的用场检测方法中的不可及代码死代码。原因在于Analyzer会模拟所有可能的执行路径——即使是沿着跳转指令的两条分支。因此任何无法由第一条指令方法入口到达的代码都不会被分析到在分析结束后无论使用什么样的Interpreter实现Analyzer.getFrames()返回的计算帧数组中不可达指令对应的帧都是null。利用这一特性可以轻松实现一个RemoveDeadCodeAdapterpublic class RemoveDeadCodeAdapter extends MethodVisitor { String owner; MethodVisitor next; public RemoveDeadCodeAdapter(String owner, int access, String name, String desc, MethodVisitor mv) { super(ASM4, new MethodNode(access, name, desc, null, null)); this.owner owner; next mv; } Override public void visitEnd() { MethodNode mn (MethodNode) mv; AnalyzerBasicValue a new AnalyzerBasicValue(new BasicInterpreter()); try { a.analyze(owner, mn); FrameBasicValue[] frames a.getFrames(); AbstractInsnNode[] insns mn.instructions.toArray(); for (int i 0; i frames.length; i) { if (frames[i] null !(insns[i] instanceof LabelNode)) { mn.instructions.remove(insns[i]); } } } catch (AnalyzerException ignored) { } mn.accept(next); } }实现要点适配器先把整个方法缓冲成一个MethodNode树 API在visitEnd时才真正动手用BasicInterpreter构建Analyzer调用a.analyze(owner, mn)完成正向数据流分析a.getFrames()与mn.instructions.toArray()按指令下标一一对应凡是帧为null且不是LabelNode的指令即为不可达指令直接从指令链表中删除LabelNode被刻意保留虽然死标记对最终字节码没有实际影响但删除一个不可及的标记可能会破坏诸如LocalVariableNode对它的引用因此保留是安全的AnalyzerException分析过程中指令类型/操作数不匹配等被忽略保证转换失败时不至于中断整个类加载流程。结合 7.1接口和组件.md 第 7.1.5 节的OptimizeJumpTransformer把跳到 GOTO折叠为跳到最终目标并把GOTO 一条 RETURN 指令替换为 RETURN 本身跳转优化引入的死代码即可被上述适配器顺势清理。对checkAndSetF方法应用这条适配器链的效果如下在 OptimizeJump 之后在 RemoveDeadCode 之后ILOAD 1ILOAD 1IFLT labelIFLT labelALOAD 0ALOAD 0ILOAD 1ILOAD 1PUTFIELD ...PUTFIELD ...RETURNRETURNlabel:label:F_SAMEF_SAMENEW ...NEW ...DUPDUPINVOKESPECIAL ...INVOKESPECIAL ...ATHROWATHROWend:end:注意上表end:标记并未被移除这正是前面强调的死标记保留策略但真正死掉的GOTO end指令已消失方法逻辑不变而字节码更精简。四、基本数据流验证器BasicVerifier 捕获非法指令序列BasicVerifier扩展自BasicInterpreter使用的是同样的 7 个值集但区别在于它会验证指令的使用是否正确。例如模拟IADD时BasicInterpreter只是简单地返回INT_VALUE而BasicVerifier会先确认它的两个操作数确实是INT_VALUE否则抛出AnalyzerException。由此它可以检测出像ISTORE 1后面紧跟ALOAD 1把 int 存入变量 1、再以引用类型加载这类无效序列。在开发类生成器或适配器时可以用它做调试对应 3.3工具.md 中工具类的用法把它包装成一个实用工具适配器任意转换链的末端接上它即可在visitEnd阶段对整个方法做一次合法性体检public class BasicVerifierAdapter extends MethodVisitor { String owner; MethodVisitor next; public BasicVerifierAdapter(String owner, int access, String name, String desc, MethodVisitor mv) { super(ASM4, new MethodNode(access, name, desc, null, null)); this.owner owner; next mv; } Override public void visitEnd() { MethodNode mn (MethodNode) mv; AnalyzerBasicValue a new AnalyzerBasicValue(new BasicVerifier()); try { a.analyze(owner, mn); } catch (AnalyzerException e) { throw new RuntimeException(e.getMessage()); } mn.accept(next); } }在实际工程中更简单的做法是使用CheckMethodAdapter并配置它以BasicVerifier作为分析器见 3.3工具.md这样校验与调试可以随转换链即时生效。五、简单数据流验证器SimpleVerifier 与冗余 CHECKCAST 消除SimpleVerifier扩展自BasicVerifier进一步提升了精度它用更细的集合模拟字节码指令执行——每个类都由表示该类所有可能对象的集合代表。因此它能检测出更多的错误比如一个对象可能的值是所有Thread类型的对象却对它调用在String类中定义的方法。实现机制上SimpleVerifier使用Java 反射 API来执行与类层次结构有关的验证和计算——它会把方法引用的类加载进 JVM再基于Class.forName得到的类型做isAssignableFrom判断。这一默认行为可以通过重写该类的受保护方法改变例如替换为自定义的类加载策略。SimpleVerifier同样可用于类生成器/适配器的调试但它的能力不止于此。下面的转换演示了一个典型优化删除方法中不必要的类型转换。如果分析发现某条CHECKCAST to指令的操作数是所有from类型对象的值集且to是from的超类那么这次转换必然成功指令就是冗余的可以删除public class RemoveUnusedCastTransformer extends MethodTransformer { String owner; public RemoveUnusedCastTransformer(String owner, MethodTransformer mt) { super(mt); this.owner owner; } Override public MethodNode transform(MethodNode mn) { AnalyzerBasicValue a new AnalyzerBasicValue(new SimpleVerifier()); try { a.analyze(owner, mn); FrameBasicValue[] frames a.getFrames(); AbstractInsnNode[] insns mn.instructions.toArray(); for (int i 0; i insns.length; i) { AbstractInsnNode insn insns[i]; if (insn.getOpcode() CHECKCAST) { Frame f frames[i]; if (f ! null f.getStackSize() 0) { Object operand f.getStack(f.getStackSize() - 1); Class? to getClass(((TypeInsnNode) insn).desc); Class? from getClass(((BasicValue) operand).getType()); if (to.isAssignableFrom(from)) { mn.instructions.remove(insn); } } } } } catch (AnalyzerException ignored) { } return mt null ? mn : mt.transform(mn); } private static Class? getClass(String desc) { try { return Class.forName(desc.replace(/, .)); } catch (ClassNotFoundException e) { throw new RuntimeException(e.toString()); } } private static Class? getClass(Type t) { if (t.getSort() Type.OBJECT) { return getClass(t.getInternalName()); } return getClass(t.getDescriptor()); } }关键细节f.getStack(f.getStackSize() - 1)取出CHECKCAST指令执行前的栈顶操作数被转换的对象值从中解析出from类型to.isAssignableFrom(from)成立即意味着from类型可以被安全赋值给toCHECKCAST永远成功直接remove通过MethodTransformer责任链串联mt.transform(mn)可以把它与其它方法级转换组合使用。用 AnalyzerAdapter 实现同一优化更快但更受限对于 Java 6 类或使用COMPUTE_FRAMES选项升级到 Java 6 的类由于类文件中已携带栈映射帧可以用核心 API 的AnalyzerAdapter见 3.3工具.md 第 3.3.2 节以更高效率完成同一任务AnalyzerAdapter会在每个visitXxxInsn回调之前维护stack字段恰好是当前指令前的操作数栈状态public class RemoveUnusedCastAdapter extends MethodVisitor { public AnalyzerAdapter aa; public RemoveUnusedCastAdapter(MethodVisitor mv) { super(ASM4, mv); } Override public void visitTypeInsn(int opcode, String desc) { if (opcode CHECKCAST) { Class? to getClass(desc); if (aa.stack ! null aa.stack.size() 0) { Object operand aa.stack.get(aa.stack.size() - 1); if (operand instanceof String) { Class? from getClass((String) operand); if (to.isAssignableFrom(from)) { return; // 直接丢弃 CHECKCAST不再向下游传递 } } } } mv.visitTypeInsn(opcode, desc); } private static Class getClass(String desc) { try { return Class.forName(desc.replace(/, .)); } catch (ClassNotFoundException e) { throw new RuntimeException(e.toString()); } } }两种方案的取舍很清晰SimpleVerifier版的树 API 转换适用于任意版本类文件可结合 10.2规则.md 中关于树 API 分析前置条件——先构造MethodNode——的规则使用而AnalyzerAdapter版只对带栈映射帧的类有效但免去了完整分析开销适合在转换链中即时生效。六、用户定义的数据流分析检测潜在空指针解引用框架的真正威力在于可变部分完全开放。假定要检测出一些字段访问和方法调用的对象可能为 null例如下面的源码片段第一行的while是为了防止编译器把它识别为o 可能尚未初始化错误Object o null; while (...) { o ...; } o.m(...); // 潜在的 NullPointerException我们需要一个数据流分析能告诉我们在对应最后一行的INVOKEVIRTUAL指令处与o对应的栈底值可能为null。为此需要为引用值区分三个集合NULL只包含null值NONNULL包含所有非 null 引用值MAYBENULL包含所有引用值——它用于表示NULL与NONNULL的并集当两条控制流汇合、一方为NULL另一方为NONNULL时只能退化为可能为 null。分析规则很简单ACONST_NULL将NULL压入操作数栈而所有其他压入引用值的指令字段访问、方法调用等压入NONNULL。换句话说我们假定任意字段访问或方法调用的结果都不是 null——如果不做全程序全局分析这已经是最好的结果了。这些规则放在自定义的Interpreter子类中实现。完全从零实现可行但更简单的做法是扩展BasicInterpreter把BasicValue.REFERENCE_VALUE视为NONNULL集只重写两个方法——模拟ACONST_NULL执行的newOperation以及计算并集的mergeclass IsNullInterpreter extends BasicInterpreter { public final static BasicValue NULL new BasicValue(null); public final static BasicValue MAYBENULL new BasicValue(null); public IsNullInterpreter() { super(ASM4); } Override public BasicValue newOperation(AbstractInsnNode insn) { if (insn.getOpcode() ACONST_NULL) { return NULL; } return super.newOperation(insn); } Override public BasicValue merge(BasicValue v, BasicValue w) { if (isRef(v) isRef(w) v ! w) { return MAYBENULL; } return super.merge(v, w); } private boolean isRef(Value v) { return v REFERENCE_VALUE || v NULL || v MAYBENULL; } }随后即可利用这个IsNullInterpreter扫描方法找出所有可能触发空指针的指令public class NullDereferenceAnalyzer { public ListAbstractInsnNode findNullDereferences(String owner, MethodNode mn) throws AnalyzerException { ListAbstractInsnNode result new ArrayListAbstractInsnNode(); AnalyzerBasicValue a new AnalyzerBasicValue(new IsNullInterpreter()); a.analyze(owner, mn); FrameBasicValue[] frames a.getFrames(); AbstractInsnNode[] insns mn.instructions.toArray(); for (int i 0; i insns.length; i) { AbstractInsnNode insn insns[i]; if (frames[i] ! null) { Value v getTarget(insn, frames[i]); if (v NULL || v MAYBENULL) { result.add(insn); } } } return result; } private static BasicValue getTarget(AbstractInsnNode insn, FrameBasicValue f) { switch (insn.getOpcode()) { case GETFIELD: case ARRAYLENGTH: case MONITORENTER: case MONITOREXIT: return getStackValue(f, 0); case PUTFIELD: return getStackValue(f, 1); case INVOKEVIRTUAL: case INVOKESPECIAL: case INVOKEINTERFACE: String desc ((MethodInsnNode) insn).desc; return getStackValue(f, Type.getArgumentTypes(desc).length); } return null; } private static BasicValue getStackValue(FrameBasicValue f, int index) { int top f.getStackSize() - 1; return index top ? f.getStack(top - index) : null; } }两个方法的分工值得细读findNullDereferences用IsNullInterpreter分析给定MethodNode然后对每条指令检查其引用操作数如果有的话的可能值集是否为NULL或MAYBENULL若是则判定该指令可能导致空指针异常加入返回列表getTarget在帧f中返回与insn对象操作数相对应的Value没有对象操作数则返回null。它的核心工作是计算该值相对操作数栈顶端的偏移量这一偏移量因指令类型而异GETFIELD/ARRAYLENGTH/MONITORENTER/MONITOREXIT接收者是栈顶偏移 0PUTFIELD栈顶是待写入的值接收者是其下一位偏移 1虚方法/特殊方法/接口方法调用接收者在所有实参之下偏移量等于实参个数Type.getArgumentTypes(desc).length。这个例子完整展示了框架固定算法 可变语义的设计Analyzer负责所有路径的遍历与帧的推进而 null 语义的建模、并集的退化规则完全收敛在 20 余行的IsNullInterpreter里。七、控制流分析基于重写 newControlFlowEdge 计算圈复杂度控制流分析有许多应用一个经典例子是计算方法的圈复杂度Cyclomatic Complexity。该度量定义为圈复杂度 控制流图的边数 − 节点数 2例如checkAndSetF方法的控制流图见 8.1介绍.md 第 8.1.2 节有 11 条边、12 个节点圈复杂度为 11 − 12 2 1。这个数字很好地表征了方法的复杂程度业界普遍认为它与方法内平均 bug 数存在相关性同时也给出了正确测试一个方法所需要的建议测试场景数量。用 ASM 分析框架实现这一度量第一步是构建控制流图。正如本篇开头所说可以通过重写Analyzer.newControlFlowEdge来完成由于Analyzer将节点表示为Frame对象若想把图存进这些对象需要先扩展Frameclass NodeV extends Value extends FrameV { SetNodeV successors new HashSetNodeV(); public Node(int nLocals, int nStack) { super(nLocals, nStack); } public Node(Frame? extends V src) { super(src); } }然后提供一个Analyzer匿名子类重写newFrame两个重载分别对应按局部变量/栈大小新建帧和按源帧复制让分析过程中产生的帧全部变成Node再重写newControlFlowEdge把边记录到后继集合中。最后统计边数、节点数并计算圈复杂度public class CyclomaticComplexity { public int getCyclomaticComplexity(String owner, MethodNode mn) throws AnalyzerException { AnalyzerBasicValue a new AnalyzerBasicValue(new BasicInterpreter()) { protected FrameBasicValue newFrame(int nLocals, int nStack) { return new NodeBasicValue(nLocals, nStack); } protected FrameBasicValue newFrame(Frame? extends BasicValue src) { return new NodeBasicValue(src); } protected void newControlFlowEdge(int src, int dst) { NodeBasicValue s (NodeBasicValue) getFrames()[src]; s.successors.add((NodeBasicValue) getFrames()[dst]); } }; a.analyze(owner, mn); FrameBasicValue[] frames a.getFrames(); int edges 0; int nodes 0; for (int i 0; i frames.length; i) { if (frames[i] ! null) { edges ((NodeBasicValue) frames[i]).successors.size(); nodes 1; } } return edges - nodes 2; } }实现要点分析完成后frames数组中只有可达指令对应的帧非 null恰好对应控制流图的所有节点每个节点的successors集合大小之和即总边数newControlFlowEdge在分析过程中被回调每次回调即一条有向边src→dst对不可达代码其帧为 null不参与统计避免死代码干扰复杂度指标同理若想同时统计异常处理边可以一并重写newControlFlowExceptionEdge。需要说明的是圈复杂度同样可以用仅基于核心 API 的方式实现效率更高但代码量会大得多tree.analysis框架的价值正在于此——把图构建的细节封装在Analyzer的可重写回调中让使用者专注业务指标的计算。八、总结与延伸围绕org.objectweb.asm.tree.analysis包本文完成了从框架设计到实战的全链路梳理框架分工Analyzer/Frame承载固定的数据流算法与栈帧推进Interpreter/Value提供可变的取值集合语义二者组合可支撑从7 个基本值集到每类一个值集再到null 三分法的不同精度分析开箱即用的三个解释器BasicInterpreter基本值集模拟、BasicVerifier追加指令使用合法性验证、SimpleVerifier基于反射的类型层次验证分别支撑死代码移除、字节码校验、冗余CHECKCAST消除等场景自定义分析范式通过继承BasicInterpreter重写newOperation与merge即可构建IsNullInterpreter这样的语义专用分析器配合Frame栈操作实现对空指针解引用、类型错误等问题的定向检测控制流分析的扩展点重写Analyzer.newControlFlowEdge/newControlFlowExceptionEdge与newFrame可低成本获得方法的控制流图进而计算圈复杂度等结构指标。后续延伸阅读建议结合 3.3工具.md 了解AnalyzerAdapter、LocalVariablesSorter、AdviceAdapter等配套适配器在转换链中的配合方式结合 7.1接口和组件.md 理解MethodNode/InsnList树模型AbstractInsnNode链式存储、LabelNode/FrameNode混排等对分析结果的影响结合 10.2规则.md 注意分析包的使用前提先构造合法MethodNode、帧版本匹配等。若要在真实项目中实践仓库的 bytecode 专题 还提供了 JavaAgentASM 字节码插桩采集方法信息 与 字节码增强统一加 TryCatch 等落地案例可与本节的静态分析能力组合出完整的字节码工程方案。赞分享文档教程后端【免费下载链接】CodeGuide:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总旨在为大家提供一个清晰详细的学习教程侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助请给予支持(关注、点赞、分享)项目地址https://gitcode.com/gh_mirrors/code/CodeGuide点击查看免费下载相关推荐nfstream流数据分析框架实战指南nfstream流数据分析框架实战指南 项目介绍 nfstream https://github.com/nfstream/nfstream 是一个强大的Pytar-split/tar/asm 深度解析tar 归档流拆分与重组的元数据骨架附 Moby 分层存储实战tar split/tar/asm 深度解析tar 归档流拆分与重组的元数据骨架附 Moby 分层存储实战 MobyDocker Engine在镜像分云原生容器运行时虚拟化容器编排Ultralytics 旋转框验证器 OBBValidator 深度解析OBB 模型验证的架构、数据流与 DOTA 评估实战Ultralytics 旋转框验证器 OBBValidator 深度解析OBB 模型验证的架构、数据流与 DOTA 评估实战 旋转目标检测Oriented人工智能深度学习计算机视觉预训练创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
