一、转换方法的字节码
利用Tree Api转化方法字节码,其实也就是对MethodNode对象的InsnList的操作。通过获取InsnList的迭代器,可以直接add 或者remove方法的指令。如果需要添加比较多的指令集,那么可以把指令集分开成不同的InsnList(临时的指令集对象)再将这些子集合并。具体的代码块如下:
InsnList il = new InsnList(); il.add(...); ... il.add(...); mn.instructions.insert(i, il);
下面通过一个例子来看一下。这个例子是和之前CoreApi 中介绍方法转换的例子 http://yunshen0909.iteye.com/blog/2223935相同。对比一下两种Api的方法转换实现方式的不同。
这个例子中,我们还是对于一个Class的所有方法(除了构造器方法)注入一段计时的逻辑。整个Class我们需要先添加一个属性timer。这时候就可以堆ClassNode的fields属性进行add操作。代码块如下:
int acc = Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC; cn.fields.add(new FieldNode(acc, "timer", "J", null, null));
我们通过AddTimerTransformer类中的transform方法来实现,对ClassNode以及其MethodNode集合的操作。AddTimerTransformer 中的注入字节码逻辑实现如下:
package asm.tree.method; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.*; import java.util.Iterator; import java.util.List; /** * Created by yunshen.ljy on 2015/7/30. */ public class AddTimerTransformer { public void transform(ClassNode cn) { for (MethodNode mn : (List<MethodNode>) cn.methods) { if ("<init>".equals(mn.name) || "<clinit>".equals(mn.name)) { continue; } InsnList insns = mn.instructions; if (insns.size() == 0) { continue; } Iterator<AbstractInsnNode> j = insns.iterator(); while (j.hasNext()) { AbstractInsnNode in = j.next(); int op = in.getOpcode(); if ((op >= Opcodes.IRETURN && op <= Opcodes.RETURN) || op == Opcodes.ATHROW) { InsnList il = new InsnList(); il.add(new FieldInsnNode(Opcodes.GETSTATIC, cn.name, "timer", "J")); il.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)); il.add(new InsnNode(Opcodes.LADD)); il.add(new FieldInsnNode(Opcodes.PUTSTATIC, cn.name, "timer", "J")); insns.insert(in.getPrevious(), il); } } InsnList il = new InsnList(); il.add(new FieldInsnNode(Opcodes.GETSTATIC, cn.name, "timer", "J")); il.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)); il.add(new InsnNode(Opcodes.LSUB)); il.add(new FieldInsnNode(Opcodes.PUTSTATIC, cn.name, "timer", "J")); insns.insert(il); mn.maxStack += 4; } int acc = Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC; cn.fields.add(new FieldNode(acc, "timer", "J", null, null)); } }
注入了timer逻辑后的Class文件反编译后如下:
package asm.core.methord; public class Time { public static long timer; public Time() { } public void myCount() { timer -= System.currentTimeMillis(); byte i = 5; byte j = 10; System.out.println(j - i); System.out.println(j + i); System.out.println(j + 0); System.out.println(0 + i); timer += System.currentTimeMillis(); } public static void myMethod(int a) { timer -= System.currentTimeMillis(); System.out.println(a + 0); timer += System.currentTimeMillis(); } }
对比CoreApi 示例中的AddTimerMethodAdapter的实现,TreeApi从流式的操作字节码转换成了对于字节码集合,也就是方法字节码链表元素的操作。并且这种操作是可以非按照字节码实际偏移量来编码的,因为通过遍历所有字节码list之后对于特定字节码(return等)的逻辑注入可以不受其他字节码子集的编码位置影响(例子中我们先插入了timer += System.currentTimeMillis();在遍历结束后再插入对于下面字节码指令的实现timer -= System.currentTimeMillis();)。然后通过mn.maxStack += 4;操作maxStack属性的值,代替了像Core中需要覆盖visitMax方法(mv.visitMaxs(maxStack + 4, maxLocals);)去操作栈空间的变化。当然,整体看下来,TreeApi的操作更加便利,但代码量上来看,两种Api差距并不大。只是TreeApi更加面向对象,对开发者更加友好。
二、全局转换
之前介绍的方法转换,迁移或者注入字节码指令都需要关注和知道字节码指令的位置。字节码指令位置关系如果写错了,那么生成的指令解析和验证就会出现问题,正如前面介绍的CoreApi的实现方式,实现起来也相当复杂。但是TreeApi 提供了任意位置来注入指令的实现方法。
下面举例来看一下。还是引用之前的一个Coffee类的一段代码为例。原来的代码片段如下:
int f; public void addEspresso(int f) { if (f >= 0) { this.f = f; } else { throw new IllegalArgumentException(); } }
这段代码编译后,用javap分析的字节码指令集如下:
public void addEspresso(int); descriptor: (I)V flags: ACC_PUBLIC Code: stack=3, locals=2, args_size=2 0: iload_1 1: iflt 13 4: aload_0 5: iload_1 6: i2l 7: putfield #2 // Field f:J 10: goto 21 13: new #3 // class java/lang/IllegalArgumentException 16: dup 17: invokespecial #4 // Method java/lang/IllegalArgumentException."<init>":()V 20: athrow 21: return LineNumberTable: line 56: 0 line 57: 4 line 59: 13 line 61: 21 StackMapTable: number_of_entries = 2 frame_type = 13 /* same */ frame_type = 7 /* same */
字节码偏移位置10那一行,goto 21 直接跳转到return指令执行。这里我们把goto 21 直接替换成return 指令。通过TreeApi我们可以对指令的相对位置进行标记和转换,也就是可以通过操作指令对象的方式来update指令。实现代码如下:
package asm.tree.method; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.*; import java.util.Iterator; /** * 将GOTO label 替换成label实际跳转到的指令-RETURN * Created by yunshen.ljy on 2015/8/14. */ public class OptimizeJumpTransformer { public void transform(MethodNode mn) { InsnList insns = mn.instructions; Iterator<AbstractInsnNode> i = insns.iterator(); while (i.hasNext()) { AbstractInsnNode in = i.next(); if (in instanceof JumpInsnNode) { // 初始化label LabelNode label = ((JumpInsnNode) in).label; AbstractInsnNode target; // 循环调用,将goto XX 中的XX跳转地址记录在label变量中 while (true) { target = label; // 跳转过滤掉FrameNode 和LabelNode while (target != null && target.getOpcode() < 0) { target = target.getNext(); } if (target != null && target.getOpcode() == Opcodes.GOTO) { label = ((JumpInsnNode) target).label; } else { break; } } // 更新替换label的值(实际跳转地址) ((JumpInsnNode) in).label = label; // 如果指令是goto ,并且新的跳转的目标指令是ARETURN 指令,那么就将当前的指令替换成这个return指令的一个clone对象 if (in.getOpcode() == Opcodes.GOTO && target != null) { int op = target.getOpcode(); if ((op >= Opcodes.IRETURN && op <= Opcodes.RETURN) || op == Opcodes.ATHROW) { // replace ’in’ with clone of ’target’ insns.set(in, target.clone(null)); } } } } } }
测试方法的代码片段如下:
ClassReader cr = new ClassReader("bytecode.Coffee"); ClassNode cn = new ClassNode(); cr.accept(cn, 0); OptimizeJumpTransformer at = new OptimizeJumpTransformer(); List<MethodNode> methodNodes = cn.methods; for(MethodNode mn :methodNodes){ if(mn.name.equals("addEspresso")){ at.transform(mn); } }
这时候可以对比一下CoreApi 的实现方式,我们不再需要关注字节码指令的绝对位置,也不再需要处理JVM的栈图表。转换后字节码指令如下:
public void addEspresso(int); descriptor: (I)V flags: ACC_PUBLIC Code: stack=3, locals=2, args_size=2 0: iload_1 1: iflt 11 4: aload_0 5: iload_1 6: i2l 7: putfield #21 // Field f:J 10: return 11: new #23 // class java/lang/IllegalArgumentException 14: dup 15: invokespecial #24 // Method java/lang/IllegalArgumentException."<init>":()V 18: athrow 19: return LocalVariableTable: Start Length Slot Name Signature 0 20 0 this Lbytecode/Coffee; 0 20 1 f I LineNumberTable: line 56: 0 line 57: 4 line 59: 11 line 61: 19 StackMapTable: number_of_entries = 2 frame_type = 11 /* same */ frame_type = 7 /* same */
三、MethodNode 源码解析
TreeApi其实在ASM中不是独立的接口,通过和CoreApi的接口和组件结合,提供了更加友好的实现。这里以MethodNode为例。可以看到源码中MethodNode 继承于MethodVisitor。并且提供了两个accept方法,分别接受ClassVisitor以及MethodVisitor参数。accept方法处理了给予MethodNode的fileds的一组事件。MethodNode本身就成为了事件的接收方。
Accept方法源码如下:
/** * Makes the given method visitor visit this method. * * @param mv * a method visitor. */ public void accept(final MethodVisitor mv) { // visits the method parameters int i, j, n; n = parameters == null ? 0 : parameters.size(); for (i = 0; i < n; i++) { ParameterNode parameter = parameters.get(i); mv.visitParameter(parameter.name, parameter.access); } // visits the method attributes if (annotationDefault != null) { AnnotationVisitor av = mv.visitAnnotationDefault(); AnnotationNode.accept(av, null, annotationDefault); if (av != null) { av.visitEnd(); } } n = visibleAnnotations == null ? 0 : visibleAnnotations.size(); for (i = 0; i < n; ++i) { AnnotationNode an = visibleAnnotations.get(i); an.accept(mv.visitAnnotation(an.desc, true)); } n = invisibleAnnotations == null ? 0 : invisibleAnnotations.size(); for (i = 0; i < n; ++i) { AnnotationNode an = invisibleAnnotations.get(i); an.accept(mv.visitAnnotation(an.desc, false)); } n = visibleTypeAnnotations == null ? 0 : visibleTypeAnnotations.size(); for (i = 0; i < n; ++i) { TypeAnnotationNode an = visibleTypeAnnotations.get(i); an.accept(mv.visitTypeAnnotation(an.typeRef, an.typePath, an.desc, true)); } n = invisibleTypeAnnotations == null ? 0 : invisibleTypeAnnotations .size(); for (i = 0; i < n; ++i) { TypeAnnotationNode an = invisibleTypeAnnotations.get(i); an.accept(mv.visitTypeAnnotation(an.typeRef, an.typePath, an.desc, false)); } n = visibleParameterAnnotations == null ? 0 : visibleParameterAnnotations.length; for (i = 0; i < n; ++i) { List<?> l = visibleParameterAnnotations[i]; if (l == null) { continue; } for (j = 0; j < l.size(); ++j) { AnnotationNode an = (AnnotationNode) l.get(j); an.accept(mv.visitParameterAnnotation(i, an.desc, true)); } } n = invisibleParameterAnnotations == null ? 0 : invisibleParameterAnnotations.length; for (i = 0; i < n; ++i) { List<?> l = invisibleParameterAnnotations[i]; if (l == null) { continue; } for (j = 0; j < l.size(); ++j) { AnnotationNode an = (AnnotationNode) l.get(j); an.accept(mv.visitParameterAnnotation(i, an.desc, false)); } } if (visited) { instructions.resetLabels(); } n = attrs == null ? 0 : attrs.size(); for (i = 0; i < n; ++i) { mv.visitAttribute(attrs.get(i)); } // visits the method's code if (instructions.size() > 0) { mv.visitCode(); // visits try catch blocks n = tryCatchBlocks == null ? 0 : tryCatchBlocks.size(); for (i = 0; i < n; ++i) { tryCatchBlocks.get(i).updateIndex(i); tryCatchBlocks.get(i).accept(mv); } // visits instructions instructions.accept(mv); // visits local variables n = localVariables == null ? 0 : localVariables.size(); for (i = 0; i < n; ++i) { localVariables.get(i).accept(mv); } // visits local variable annotations n = visibleLocalVariableAnnotations == null ? 0 : visibleLocalVariableAnnotations.size(); for (i = 0; i < n; ++i) { visibleLocalVariableAnnotations.get(i).accept(mv, true); } n = invisibleLocalVariableAnnotations == null ? 0 : invisibleLocalVariableAnnotations.size(); for (i = 0; i < n; ++i) { invisibleLocalVariableAnnotations.get(i).accept(mv, false); } // visits maxs mv.visitMaxs(maxStack, maxLocals); visited = true; } mv.visitEnd(); }
这样我们就可以将CoreApi和TreeApi 结合起来,用CoreApi处理Class,TreeApi处理Method。这样在我们自己的ClassVisitorAdapter中就可以用下面的方式来处理method中的指令对象集合:
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { if (name.startsWith("is")) { // System.out.println(" start with is method: " + name + desc); } return new MethodNode(Opcodes.ASM5, access, name, desc, signature, exceptions) { @Override public void visitEnd() { accept(cv); } }; }
相关推荐
在`visitMethod`方法中,我们需要在方法开始前(`visitCode`之前)插入切面逻辑,然后继续遍历原方法的字节码,最后在方法结束(`visitEnd`)时添加切面逻辑。 4. 在方法的字节码流中,使用`visitInsn`系列方法插入...
【标题】"cglib-2.2.jar asm-tree.jar asm-commons.jar asm.jar" 提供的是一组用于Java编程的库,它们主要用于实现动态代理和字节码操作。 【描述】"cglib动态代理模式jar包 cglib-2.2.jar asm-tree.jar asm-...
《ASM Tree库详解》 ...无论是用于动态代理、AOP实现,还是用于代码插桩或逆向工程,ASM Tree库都是Java开发者的得力助手。理解并掌握ASM Tree库的使用,无疑将提升你在Java字节码操作领域的专业能力。
"asm-tree-3.3.jar"是ASM Tree模块的主文件,包含了实现ASM Tree API的所有类和方法。而"asm-2.2.1.jar.license.txt"很可能包含的是ASM框架2.2.1版本的许可协议文本,用户在使用ASM库时需要遵循这些条款。 关于ASM ...
ASM注入器工具是一种专门设计用于调试和远程注入CALL指令的实用程序。在IT行业中,ASM(Assembly Language)注入是高级编程技术的一种,它涉及到在运行时修改或插入汇编代码到另一个进程,以改变其行为或功能。这种...
`asm-tree`是Java字节码处理框架ASM的一个组件,主要负责将字节码解析成抽象语法树(Abstract Syntax Tree, AST)。ASM是一个强大的库,它允许程序动态生成和分析Java类。在Java世界中,这通常用于创建元编程框架、...
《ASM Tree库3.2版本详解与应用》 ASM是一个Java字节码操控和分析框架,它可以直接解析和生成Java字节码...通过理解其核心概念、版本特性以及实践操作,开发者能够更好地利用ASM Tree来实现复杂的代码生成和分析任务。
这个JAR文件包含了所有的类和资源,使得我们能够在程序中直接引用ASM Tree的相关API,进行字节码操作和解析。 "asm-2.2.1.jar.license.txt"则可能是ASM库的许可协议文件,详细规定了使用ASM库的法律条款和限制。...
这篇博客"ASM函数监听实现(二)之打印注入函数的参数值"深入探讨了如何利用ASM库来监控并记录函数调用时的参数值,这对于调试、日志记录以及性能分析等场景非常有用。 首先,我们来看`asmAopClassAdapter.java`。...
例如,可以使用ASM Tree API遍历一个类的整个字节码结构,查找并修改特定的指令或属性,或者在运行时动态生成新的类和方法。此外,由于ASM Tree API具有较高的抽象层次,因此它适用于各种类型的Java字节码操作,无论...
为了在函数执行前拦截参数值,我们需要在`visitInsn`方法中插入自定义逻辑,这通常是在调用`invokeXXX`系列方法之前。 接下来,我们要创建一个`ClassAdapter`,这是ASM提供的一种简化类改造的工具。通过继承`Class...
《ASM Tree库3.1版本解析与应用》 ASM是一个Java字节码操控和分析框架,它可以直接操作和生成中间...通过理解和利用ASM Tree库,开发者可以在Java字节码层面上实现各种高级功能,如动态代理、代码混淆、性能优化等。
包含翻译后的API文档:asm-9.1-javadoc-API文档-中文(简体)版.zip; Maven坐标:org.ow2.asm:asm:9.1; 标签:ow2、jar包、java、中文文档; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可...
ASM Tree 2.1是该库的一个稳定版本,相比早期版本,它可能包含了一些性能优化和bug修复,提高了API的稳定性和兼容性。 二、ASM Tree的核心概念 1. `ClassNode`:ASM Tree的核心类,代表一个Java类。它包含了类的...
动态代理可以利用ASM Tree来快速生成代理类,实现方法拦截;代码生成则可以自动生成符合特定需求的Java类或方法;字节码插桩则是在编译后的字节码层面插入额外的代码,以实现性能监控、调试或者其他高级功能。 在...
在实际应用中,可能需要动态查找这个地址或者使用其他方式将API调用编码到汇编代码中。 远程线程注入虽然强大,但也存在风险。不恰当的使用可能会导致程序崩溃,而且经常被用于恶意活动,因此在使用时需谨慎。在...
1. **字节码增强**:例如,Spring AOP(面向切面编程)利用ASM动态生成代理类,实现方法拦截。 2. **动态代理**:开发者可以使用ASM创建运行时的类,提供动态接口实现。 3. **代码分析**:在性能优化、代码安全审计...
在Android无痕埋点中,ASM插桩的原理是利用Java字节码注入技术,在运行时动态插入埋点代码。当用户执行特定操作时,如点击按钮或加载页面,ASM会在对应的函数调用前后插入埋点代码,记录用户的行为并上报至服务器。...
ASM Tree还提供了访问控制符、类型信息、参数列表等元数据的API,使得开发者能够方便地操作这些元素。 对于JavaEE开发者来说,了解如何使用ASM Tree能够极大地扩展其代码的灵活性和效率。例如,通过ASM Tree可以...
asm-tree-5.0.3.jar