美文网首页bugstac...
基于javaAgent和ASM字节码技术跟踪java程序调用链

基于javaAgent和ASM字节码技术跟踪java程序调用链

作者: senju | 来源:发表于2018-11-22 09:18 被阅读1140次

    作者:李家琦        评阅人:高邱雅   鹿凯翔

    一、介绍

    1. 目的

    本文主要介绍如何使用javaAgent和ASM技术对java程序的方法调用进行跟踪,获得运行时方法之间的调用关系和方法的运行时间等信息,可以用于理解程序结构、了解方法实际执行时间、分析程序性能瓶颈等场景。使用javaAgent技术在程序加载字节码文件时,获取字节码并返回一个修改过的字节码文件,利用ASM技术可以对字节码进行增强,从而获取目标方法的运行状态。使用这种方式的好处是,可以在对代码没有入侵的情况下实现跟踪。

    2. 环境

    本项目中使用JDK8进行开发,并使用maven进行依赖管理。

    二、 javaAgent技术介绍及使用

    java.lang.Instrument包是在JDK5引入的,开发者可以通过修改方法的字节码实现动态修改类代码。下面先介绍一些相关概念:

    • JVMTI
      首先需要介绍下JVMTI(JVM Tool Interface),它是JVM暴露出来的一些供开发者扩展的接口集合,当执行到某段程序时会调用某些回调接口,开发者可以利用这些接口扩展自己的逻辑。例如,在本项目中我们希望能够在JVM加载字节码时,获取到字节码并对其进行修改。

    • JVMTIAgent
      JVMTIAgent是一个动态库,它可以利用JVMTI暴露出的一些接口来实现一些特殊的功能。它有两种加载方式,可以在程序启动时加载,也可以在程序运行时动态进行加载。在我们使用eclipse、IDEA等IDE运行或者调试java代码时,它们就会在启动程序时加入相关参数,比如使用IDEA运行java程序,留意控制台最上方的输出,就会发现类似于如下的内容:

    "D:\Programs\Java\jdk1.8.0_172\bin\java.exe" "-javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2018.2.1\lib\idea_rt.jar=52832:D:\Program Files\JetBrains\IntelliJ IDEA 2018.2.1\bin" ......
    
    • instrument agent
      instrument是JVM 提供的一个 JVMTIAgent,windows环境下可以在jdk bin目录中找到一个叫做 instrument.dll的动态链接库。

    • 使用instrument agent
      首先需要实现一个包含premain的类,,如下所示

    public class AopAgentTest {
        static private Instrumentation _inst = null;
        public static void premain(String agentArgs, Instrumentation inst) {
            Param.generatePARAMS(agentArgs);
            System.out.println("AopAgentTest.premain() was called.");
    
            /* Provides services that allow Java programming language agents to instrument programs running on the JVM.*/
            _inst = inst;
    
            /* ClassFileTransformer : An agent provides an implementation of this interface in order to transform class files.*/
            ClassFileTransformer trans = new AopAgentTransformer();
    
            /*Registers the supplied transformer.*/
            _inst.addTransformer(trans);
        }
    }
    

    在premain方法中,可以获得一个Instrumentation对象,我们可以向其中加入一个ClassFileTransformer对象,Transformer对象的实现如下所示:

    public class AopAgentTransformer implements ClassFileTransformer{
        public byte[] transform(ClassLoader loader, String className,
                                Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
                                byte[] classfileBuffer) throws IllegalClassFormatException {
            System.out.println("Transforming " + className);
            /*
             TODO
             修改字节码,并返回修改后的字节码
            */
            byte[] transformed = classfileBuffer;       
            return transformed;
        }
    }
    

    这样就可以在JVM加载字节码文件时,获取到字节码并进行修改。然后,还需要在MANIFEST.MF中加入Premain-Class,并将程序打成jar包的形式(本项目中将程序的相关依赖也一并打进jar包),以便在目标程序中使用。使用maven插件进行打包,如下所示:

    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <configuration>
            <archive>
                <manifest>
                    <addClasspath>true</addClasspath>
                    <classpathPrefix>lib/</classpathPrefix>
                    <mainClass></mainClass>
                    <addClasspath>true</addClasspath>
                    <classpathPrefix>lib/</classpathPrefix>
                </manifest>
                <manifestEntries>
                    <Class-Path>.</Class-Path>
                    <Premain-Class>com.nju.msr.core.instrument.AopAgentTest</Premain-Class>
                </manifestEntries>
            </archive>
        </configuration>
    </plugin>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-dependency-plugin</artifactId>
        <executions>
            <execution>
                <id>copy</id>
                <phase>package</phase>
                <goals>
                    <goal>copy-dependencies</goal>
                </goals>
                <configuration>
                    <outputDirectory>${project.build.directory}/lib</outputDirectory>
                </configuration>
            </execution>
        </executions>
    </plugin>
    

    最后,在运行目标程序时,只需要添加 javaagent参数即可,如下所示:

    -javaagent:./test.jar
    

    需要注意的是,一个java程序中-javaagent这个参数的个数是没有限制的,所以可以添加任意多个javaagent。所有的javaagent会按照运行时的参数顺序执行。此外,javaagent需要放在包含main方法的jar包之前,否则javaagent不会起作用。每一个java agent 都可以接收一个字符串类型的参数,也就是premain中的agentArgs。

    三、 ASM技术介绍及使用

    • 首先我们需要引入如下的库:
    <dependency>
        <groupId>org.ow2.asm</groupId>
        <artifactId>asm</artifactId>
        <version>6.2.1</version>
    </dependency>
    <dependency>
        <groupId>org.ow2.asm</groupId>
        <artifactId>asm-util</artifactId>
        <version>6.2.1</version>
    </dependency>
    <dependency>
        <groupId>org.ow2.asm</groupId>
        <artifactId>asm-commons</artifactId>
        <version>6.2.1</version>
    </dependency>
    
    • ASM使用观察者模式,依次访问类中的每个方法、属性,我们首先实现一个ClassVistior和MethodVisitor
    public class ClassAdapter extends ClassVisitor implements Opcodes {
    
        private String owner;
        private boolean isInterface;
    
        public ClassAdapter(final ClassVisitor cv) {
            super(ASM6, cv);
        }
    
        @Override
        public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
            cv.visit(version, access, name, signature, superName, interfaces);
            owner = name;
            isInterface = (access & Opcodes.ACC_INTERFACE) != 0;
        }
    
        @Override
        public MethodVisitor visitMethod(final int access, final String name,
                                         final String desc, final String signature, final String[] exceptions) {
            MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
    
            if (!isInterface && mv != null && !"<init>".equals(name) && !"<clinit>".equals(name)) {
                mv = new MethodAdapter(mv, owner, access, name, desc, signature, exceptions);
            }
            return mv;
        }
    }
    

    当访问到类中的每个方法时,会调用visitMethod方法,产生一个MethodVisitor对象来访问这个方法。在这里忽略了构造函数和类的静态代码块。对于java中的构造函数,我们知道它第一个调用的方法一定是父类的构造函数,当对字节码修改构造函数时,情况会有些复杂,这里暂不讨论。下面我们来实现一个MethodVisitor对象:

    public class MethodAdapter extends MethodVisitor implements Opcodes {
    
        protected String className = null;
        protected int access = -1;
        protected String name = null;
        protected String desc = null;
        protected String signature = null;
        protected String[] exceptions = null;
    
        public MethodAdapter(final MethodVisitor mv, final String className, final int access, final String name,
                              final String desc, final String signature, final String[] exceptions) {
            super(ASM6, mv);
            this.className = className;
            this.access = access;
            this.name = name;
            this.desc = desc;
            this.signature = signature;
            this.exceptions = exceptions;
        }
        @Override
        public void visitCode() {
            super.visitCode();
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "err", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("CALL classname:"+ className+ " access"+access+" name:" + name +" desc"+desc+" singature:"+signature);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        }
    
        @Override
        public void visitInsn(int opcode) {
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "err", "Ljava/io/PrintStream;");
                mv.visitLdcInsn("CALL classname:"+ className+ " access"+access+" name:" + name +" desc"+desc+" singature:"+signature);
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            }
            super.visitInsn(opcode);
        }
    }
    

    当开始访问一个方法时,会调用visitCode方法,我们在这里加入一段输出“方法开始信息”的代码,当访问每个操作前会调用visitInsn,我们获取它的操作类型,如果是返回类型或者是抛出异常,则输出方法结束的信息。
    要学会使用ASM操作字节码,需要对java字节码有一定的了解,这里只对涉及到的相关内容做简要介绍。
    JVM中使用栈来操作数据,例如:

    System.out.println("hello word");
    

    编译成字节码后,根据字节码的规则可翻译成如下结果(使用javap可以查看java编译器生成的字节码文件,或者使用IDE中的相关插件):

    getstatic                       // Field java/lang/System.out:Ljava/io/PrintStream;
    ldc                            // String hello word
    invokevirtual                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    

    其中ldc是一个常量入栈指令,将字符串 hello word的引用入栈,然后用方法调用指令invokevirtual调用println方法,从栈顶获得操作数,并出栈。根据字节码可以查找到ASM中的相关方法。
    最后在ClassFileTransform中调用即可:

    byte[] transformed = null;
    try {
        ClassReader cr = new ClassReader(new java.io.ByteArrayInputStream(classfileBuffer));
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        ClassAdapter ca = new ClassAdapter(cw);
        cr.accept(ca, ClassReader.EXPAND_FRAMES);
        transformed = cw.toByteArray();
    }catch (RuntimeException re){
        re.printStackTrace();
    }catch (IOException e) {
        System.err.println("can't transform "+ className+"  "+e);
        e.printStackTrace();
    }
    return transformed;
    

    四、使用ASM跟踪目标程序调用链

    1. 基本思路

    • 为了获得方法的调用链信息,我们需要在每个方法的开始和结束加入收集信息的方法,这里我们不详细讨论如何处理收集到的信息,只关注于如何收集信息。我们新建一个类Actions,如下:
    public class Actions {
        static public final String path = Actions.class.getName().replace('.','/');
        static public void methodStart(String owner, String name, String desc){
            //处理操作
        }
        static public void methodEnd(String owner, String name, String desc){
            //处理操作
        }
    }
    

    并在方法的开始和结束部分分别调用Actions的methodStart和methodEnd方法

    mv.visitLdcInsn(className);
    mv.visitLdcInsn(name);
    mv.visitLdcInsn(desc);
    mv.visitMethodInsn(INVOKESTATIC, Actions.path,"methodStart","(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",false);
    

    2. 对程序中抛出异常的处理

    • 在上述过程中,我们在方法的开头调用了methodStart方法,在所有抛出异常和return操作之前调用了methodEnd方法,以此来收集调用信息。但是,抛出异常并不代表方法一定结束了,而且对于运行时异常的情况,上述的方法也无能为力。我们知道java中提供了try finally的方式可以确保我们在代码块结束时运行某段程序,而字节码中并没有这样的操作。在字节码中try finally会以如下形式转换:

    源代码

    try {
        System.out.println();
    }finally {
        System.out.println();
    }
    

    字节码

    Code:
       0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: invokevirtual #4                  // Method java/io/PrintStream.println:()V
       6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       9: invokevirtual #4                  // Method java/io/PrintStream.println:()V
      12: goto          24
      15: astore_1
      16: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      19: invokevirtual #4                  // Method java/io/PrintStream.println:()V
      22: aload_1
      23: athrow
      24: return
    Exception table:
       from    to  target type
           0     6    15   any
    

    Exception table中表示:从0到6行如果抛出了任何异常,跳转到15行,在12行goto到了24行方法的结尾。所以,我们可以使用捕获所有异常的方式,来实现try finally的操作。

    五、总结

    • 本文中使用了javaAgent和ASM在程序运行时修改字节码,获取了调用链信息
    • 在ClassFileTransformz中我们可以只修改我们需要监控的类,对于那些没有监控的方法,可以考虑通过堆栈信息获得。注意并不是所有的字节码文件都能获取到并修改,有些在ClassFileTransform 对象加入之前就加载好的类,就没法再次获得了。使用javaagent也可以对已加载类的字节码做变更,但是这种情况下会有很多的限制。

    相关文章

      网友评论

        本文标题:基于javaAgent和ASM字节码技术跟踪java程序调用链

        本文链接:https://www.haomeiwen.com/subject/zkpkqqtx.html