美文网首页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