相关资讯

ag九游会官方咱们将会使用 ASM 竣事一个调用链追踪-九游会J9·(china)官方网站-真人游戏第一品牌

发布日期:2024-06-25 06:50    点击次数:139

在本篇著述中,你将学会如何使用 ASM 框架对 Java 的 class 文献进行打桩(Instrument)。Part 1 先容 Java 字节码联系常识并展示如何阅读 class 文献ag九游会官方,Part 2 先容 ASM 中凡俗用到的造访者模式(Visitor),临了在 Part 3 咱们将会使用 ASM 搭建一个节略的调用追踪打桩样例。

Part 1:Java 字节码

ASM 是一个 Java 字节码操作框架。率先咱们先弄明晰什么是“Java 字节码”,Java 字节码是 Java 造谣机中的提醒集。每条提醒由一个单字节的操作码加上零或多个操作数构成。举例,“iadd”需要接纳两个整数动作操作数,然后该提醒将它们加起来。对于提醒集的详备信息不错参考 这里 。底下这个分组列表会匡助你快速了解 Java 字节码包含哪些:

加载和存储(举例 aload_0,istore)算术运算和逻辑运算(Iadd,fcmpl)类型调遣(i2b,d2i)对象创建和操作(new,putfield)操作数栈操作(swap,dup2)戒指移动(ifeq,goto)门径调用和复返(invokespecial,areturn)

Java 造谣机:

在深入字节码之前,咱们先来弄明晰在字节码实践进程中 java 造谣机(JVM)是怎样使命的。JVM 是一个平台无关的实践环境,它将 Java 字节码调遣成机器话语何况实践。何况,JVM 是一个基于栈的造谣机,每个线程齐有一个 JVM 栈,这个栈由栈帧(Frame)构成。每次调用一个门径时齐会创建一个栈帧,这个栈帧由操作数栈、腹地变量表和指向着手时时量池的援用构成。

对于 JVM 更多内容详见 这里

基于栈的造谣机:

为了更好的意会 Java 字节码,咱们需要知谈极少对于基于栈的 VM。对于一个基于栈的造谣机来说,存放着操作数的内存结构是栈。操作数以后进先出(LIFO)的形式从栈中弹出,然后措置,临了再把结果 push 且归。举个例子,两个数相加的举止如下所示:

要是你对这部分比拟感兴味,那么不错参考 这里 获得更多对于基于栈的造谣机和基于寄存器的造谣机的联系常识。

底下来看一个 Java 代码的例子:

public class Test { public static void main(String[] args) { printOne(); printOne(); printTwo(); } public static void printOne() { System.out.println("Hello World"); } public static void printTwo() { printOne(); printOne(); }}

咱们使用“javac”来编译这段枢纽生成 class 文献,然后使用“javap -c”领会 class 文献来得到字节码,如下所示:

public class Test { public Test(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return public static void main(java.lang.String[]); Code: 0: invokestatic #2 // Method printOne:()V 3: invokestatic #2 // Method printOne:()V 6: invokestatic #3 // Method printTwo:()V 9: return public static void printOne(); Code: 0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #5 // String Hello World 5: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return public static void printTwo(); Code: 0: invokestatic #2 // Method printOne:()V 3: invokestatic #2 // Method printOne:()V 6: return}

率先来看 Test()构造门径,构造门径的字节码包含三条字节码提醒。第一条字节码提醒 aload_0 将腹地变量表中下标为 0 的变量 push 进操作数栈中,该变量为构造门径的隐含门径参数 this。第二条提醒 invokespecial 调用父类的构造器。系数不深入剿袭的类齐隐式剿袭于 java.lang.Object,编译器添加了必要的字节码来调用基类的构造器。在这条提醒实践中,操作数栈的顶部值会被弹出。不错看到字节码中左边的索引值不畅通,这是因为一些字节码需要有参数,参数在字节码数组中占用位置。#number 是常量池中的常量索引,常量池是一个表,它包含字符串常量,类和接口称号,字段称号和其他在 Class 文献结构顶用到的常量。咱们不错使用 javap -c -v 来看通盘常量池。Java 中有两种类型的门径:实例门径(invokevirtual)和类门径(invokestatic)。当 Java 造谣机调用类门径,它基于对象援用类型来调用门径,这是编译时就不错细方针;而造谣机实践实例门径时,它基于对象本色类型来调用门径,这是着手时细方针;对于更多 Java 字节码的内容不错看这篇 得力的著述

Part 2:造访者模式

在面向对象编程中,造访者模式是一种分裂对象结构和操作算法的模式,这种分裂或者让咱们在不修改原结构的情况下添加新的操作。

有计划两个对象,它们的类不同;一个称为元素(Element),另一个称为造访者(Visitor)。元素有一个 accept 门径,该门径招揽造访者动作参数;accept()门径调用造访者的 visit()门径,何况将元素本人动作参数传递给造访者。

代码样举例下。在这个例子中,咱们将会依照 ASM 在字节码操作中使用的造访者模式来编写,因此代码结构会和 ASM 有点访佛。

添加一个 accept(Visitor)门径到元素类中创建一个造访者基类,基类中包含每一种元素类的 visit()门径创建一个造访者派生类,派生类竣事基类的各式 visit 门径Client 创建造访者对象,调用元素 accept()门径并传递造访者对象

interface Element { // 1. accept(Visitor) interface public void accept( Visitor v ); // first dispatch} class This implements Element { // 1. accept(Visitor) implementation public void accept( Visitor v ) { v.visit( this ); } public String thiss() { return "This"; }}class That implements Element { public void accept( Visitor v ) { v.visit( this ); } public String that() { return "That"; }}class TheOther implements Element { public void accept( Visitor v ) { v.visit( this ); } public String theOther() { return "TheOther"; }}// 2. Create a "visitor" base class with a visit() method for every "element" typeinterface Visitor { public void visit( This e ); // second dispatch public void visit( That e ); public void visit( TheOther e );}// 3. Create a "visitor" derived class for each "operation" to perform on "elements"class UpVisitor implements Visitor { public void visit( This e ) { System.out.println( "do Up on " + e.thiss() ); } public void visit( That e ) { System.out.println( "do Up on " + e.that() ); } public void visit( TheOther e ) { System.out.println( "do Up on " + e.theOther() ); }}class DownVisitor implements Visitor { public void visit( This e ) { System.out.println( "do Down on " + e.thiss() ); } public void visit( That e ) { System.out.println( "do Down on " + e.that() ); } public void visit( TheOther e ) { System.out.println( "do Down on " + e.theOther() ); }}class VisitorDemo { public static Element[] list = { new This(), new That(), new TheOther() }; // 4. Client creates "visitor" objects and passes each to accept() calls public static void main( String[] args ) { UpVisitor up = new UpVisitor(); DownVisitor down = new DownVisitor(); for (int i=0; i < list.length; i++) { list[i].accept( up ); } for (int i=0; i < list.length; i++) { list[i].accept( down ); } }}

着手结果如下:

do Up on This do Down on Thisdo Up on That do Down on Thatdo Up on TheOther do Down on TheOther

ASM 中的造访者模式:

在 ASM 中,元素(Element)为 ClassReader 类、MethodNode 类等等,造访者接口则包含 ClassVisitor,AnnotationVisitor,FieldVisitor 和 MethodVisitor。MethodNode 类中的 accept 门径有如下门径签名:

void accept(ClassVisitor cv)void accept(MethodVisitor mv)

ClassVisitor 中的 visit 门径族如下:

void visit(int version, int access, String name, String signature, String superName, String[] interfaces) AnnotationVisitor visitAnnotation(String desc, boolean visible)void visitAttribute(Attribute attr)void visitEnd()FieldVisitor visitField(int access, String name, String desc, String signature, Object value)void visitInnerClass(String name, String outerName, String innerName, int access)MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions)void visitOuterClass(String owner, String name, String desc)void visitSource(String source, String debug)

Part 3:调用链追踪

在本节中,咱们将会使用 ASM 竣事一个调用链追踪,代码中将会打印每个门径调用和复返。待会不错看到,打印出来的日记不错被很容易措置成一个高下文调用树。

在不时之前,你需要装置 JDK 环境,同期下载 ASM 5.0.3 binary distribution 。另外,样例代码不错在 这里 找到。解压 asm 和样例包,将 asm-all-5.0.3.jar 复制到样例代码目次下:

$ unzip ASM-tutorial.zip$ unzip asm-5.0.3-bin.zip$ cp asm-5.0.3/lib/all/asm-all-5.0.3.jar ASM-tutorial/

Hello ASM:复制类文献

为了纯属 ASM 使用门径,咱们第一个 ASM 枢纽仅仅节略的复制一个 class 文献。后头咱们会作念更有真理的事情,但其实和这个例子的结构差未几。咱们的 Copy.java 代码如下:

import java.io.FileInputStream;import java.io.FileOutputStream; import org.objectweb.asm.ClassReader;import org.objectweb.asm.ClassWriter;public class Copy {public static void main(final String args[]) throws Exception {FileInputStream is = new FileInputStream(args[0]); ClassReader cr = new ClassReader(is); ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); cr.accept(cw, 0); FileOutputStream fos = new FileOutputStream(args[1]); fos.write(cw.toByteArray()); fos.close();}}

这个 Copy 枢纽需要招揽两个大叫行参数,args[0]是原 class 文献称号,args[1]是方针 class 称号。

咱们在例子中使用了两个 ASM 类: ClassReader 从文献中读取 Java 字节码, ClassWriter 写字节码到文献中。ASM 使用上头提到的 造访者模式 :ClassWriter 竣事了 ClassVisitor ,然后通过 cr.accept(cw, 0)来使得 ClassReader 在遍历字节码进程中握住调用 cw 的 visit 门径,最终产生相通的字节码序列。

ClassWriter 构造门径中的 ClassWriter.COMPUTE_FRAMES 参数是可选的,它使得 ClassWriter 自动贪图栈帧大小。cr.accept 门径的第二个参数相通为可选参数,0 示意默许举止。具体请参考 ClassReader 和 ClassWriter 的 JavaDocs。

# Compile Copy$ javac -cp asm-all-5.0.3.jar Copy.java# Use Copy to copy itself$ java -cp .:asm-all-5.0.3.jar Copy Copy.class Copy2.class

调用链追踪

咱们还是纯属了 ASM 的基本使用,那么现时来竣事调用链追踪。咱们将会使用 stderr 打印门径的调用和复返,假定原始枢纽如下:

public class Test { public static void main(String[] args) { printOne(); printOne(); printTwo(); } public static void printOne() { System.out.println("Hello World"); } public static void printTwo() { printOne(); printOne(); }}

咱们将会进行代码打桩,在门径调用前后输出信息到 stderr。对 Test.class 打桩后的效劳如下所示:

public class TestInstrumented {public static void main(String[] args) {System.err.println("CALL printOne");printOne();System.err.println("RETURN printOne"); System.err.println("CALL printOne");printOne();System.err.println("RETURN printOne");System.err.println("CALL printTwo");printTwo();System.err.println("RETURN printTwo");}public static void printOne() {System.err.println("CALL println");System.out.println("Hello World");System.err.println("RETURN println");}public static void printTwo() {System.err.println("CALL printOne");printOne();System.err.println("RETURN printOne");System.err.println("CALL printOne");printOne();System.err.println("RETURN printOne");}}

咱们将通过修改上头的 Copy 代码样例来竣事代码打桩。为了修改 class 文献,咱们需要在 ClassReader 和 ClassWriter 之间插入一些代码。这会使用到 适配器模式 ,适配器包装了一个对象何况笼罩该对象的一些门径,在这些笼罩门径中调用其他对象的门径。这让咱们很便捷的修改被包装对象的举止。这里咱们对 ClassWriter 作念适配,当产生调用门径的字节码时,咱们在调用前后加入打印追踪日记的代码。

由于门径调用出现时线法中,咱们主要的打桩使命会在门径声明里进行。这么会稍稍有点复杂,因为门径声明是包含在类里的,咱们需要遍历一个类来对它的门径打桩。

第一步,咱们需要使用底下的 ClassAdapter 来对 ClassWriter 作念适配。大部分情况下,ClassAdapter 中剿袭于 ClassVisitor 的门径仅仅节略调用被适配的 ClassWriter 的相通门径;咱们只笼罩 ClassWriter.visitMethod 门径,这个门径在际遇类门径声明时会被调用。visitMethod 的复返值是一个 MethodVisitor 对象,这个对象会被用来措置门径体。ClassWriter.visitMethod 复返一个 MethodVisitor,而 MethodVisitor 会产生门径的字节码。咱们需要对 ClassWriter.visitMethod 复返的 MethodVisitor 作念适配,插入极度的提醒来打印调用链。

class ClassAdapter extends ClassVisitor implements Opcodes { public ClassAdapter(final ClassVisitor cv) {super(ASM5, cv);}@Overridepublic MethodVisitor visitMethod(final int access, final String name,final String desc, final String signature, final String[] exceptions) {MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);return mv == null ? null : new MethodAdapter(mv);}}class MethodAdapter extends MethodVisitor implements Opcodes {public MethodAdapter(final MethodVisitor mv) {super(ASM5, mv);}@Overridepublic void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {/* TODO: System.err.println("CALL" + name); *//* do call */mv.visitMethodInsn(opcode, owner, name, desc, itf);/* TODO: System.err.println("RETURN" + name); */}}

到现时为止,咱们的 MethodAdapter 类莫得添加任何的打桩代码,它仅仅节略调用被包装的 MethodVisitor——mv。咱们知谈怎样使用 Java 语法来打桩,但咱们不知谈怎样用 ASM 的 API 来竣事。咱们不错用 ASM 中自带的 ASMifier 这个器用来匡助咱们分析。

咱们不错使用 ASMifier 来将 TestInstrumented 调遣成 ASM API 调用。为了精真金不怕火,这里不详了一些无关代码:

$ javac TestInstrumented.java$ java -cp .:asm-all-5.0.3.jar org.objectweb.asm.util.ASMifier TestInstrumented/** WARNING: THINGS ARE ELIDED **/{mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "printOne", "()V", null, null);mv.visitCode(); mv.visitFieldInsn(GETSTATIC, "java/lang/System", "err", "Ljava/io/PrintStream;");mv.visitLdcInsn("CALL println");mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");mv.visitLdcInsn("Hello World");mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);mv.visitFieldInsn(GETSTATIC, "java/lang/System", "err", "Ljava/io/PrintStream;");mv.visitLdcInsn("RETURN println");mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);mv.visitInsn(RETURN);mv.visitMaxs(2, 0);mv.visitEnd();}/** WARNING: MORE THINGS ARE ELIDED **/

ASMifier 的输出是一个 ASM 枢纽,这段枢纽不错用来着手产生 TestInstrumented.class。其中咱们念念知谈的是如何调用 System.err.println:

mv.visitFieldInsn(GETSTATIC, "java/lang/System", "err", "Ljava/io/PrintStream;");mv.visitLdcInsn("CALL println");mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

现时咱们知谈怎样调用 System.err.println,咱们不错完成 MethodAdapter 的竣事了:

class MethodAdapter extends MethodVisitor implements Opcodes { public MethodAdapter(final MethodVisitor mv) { super(ASM5, mv); } @Override public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { /* System.err.println("CALL" + name); */ mv.visitFieldInsn(GETSTATIC, "java/lang/System", "err", "Ljava/io/PrintStream;"); mv.visitLdcInsn("CALL " + name); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); /* do call */ mv.visitMethodInsn(opcode, owner, name, desc, itf); /* System.err.println("RETURN" + name); */ mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "err", "Ljava/io/PrintStream;"); mv.visitLdcInsn("RETURN " + name); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); }}

现时,大功凯旋!咱们现时来试试 Test 代码样例的调用链追踪吧:

# Build Instrumenter$ javac -cp asm-all-5.0.3.jar Instrumenter.java # Build Example$ javac Test.java# Move Test.class out of the way$ cp Test.class Test.class.bak# Instrument Test$ java -cp .:asm-all-5.0.3.jar Instrumenter Test.class.bak Test.class# Run!$ java TestCALL printOneCALL printlnHello WorldRETURN printlnRETURN printOneCALL printOneCALL printlnHello WorldRETURN printlnRETURN printOneCALL printTwoCALL printOneCALL printlnHello WorldRETURN printlnRETURN printOneCALL printOneCALL printlnHello WorldRETURN printlnRETURN printOneRETURN printTwo

现时你知谈怎样使用 ASM 来进行代码打桩了ag九游会官方。