前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >实现一个javaagent需要几步?

实现一个javaagent需要几步?

作者头像
tnt阿信
发布2021-11-11 14:45:37
6550
发布2021-11-11 14:45:37
举报

在介绍javaagent之前,我想有必要向大家介绍一下JVMTI,因为javaagent是基于这个技术实现的

JVMTI

JVMTI(JVM Tool Interface)是 Java 虚拟机所提供的 native 编程接口,JVMTI可以用来开发并监控JVM,可以查看JVM的内部状态,并控制JVM应用程序的执行。

JVMTI只是一套接口,我们要开发JVM工具就需要写一个Agent程序来使用这些接口。Agent程序其实就是一个C/C++语言编写的动态链接库

注:这里提到的agent程序和javaagent不是同一概念

我们通过JVMTI开发好agent程序后,把程序编译成动态链接库,之后可以在jvm启动时指定加载运行该agent。

代码语言:javascript
复制
-agentlib:<agent-lib-name>=<options>

之后JVM启动后该agent程序就会开始工作。

而接下来要提到的Instrumention机制,也是通过实现了一个JVMTI的agent来完成的,这个agent的实现代码在libinstrument.so里(在BSD系统中叫做libinstrument.dylib),由于libinstrument.so是java内置的,所以不需要我们手动通过-agentlib参数指定就可以使用它

这个动态链接库可以在{JAVA_HOME}/jre/lib下找到,除此之外,还能看到和调试相关的agent实现——libjdwp.dylib

Instrumention 机制

有了Instrumention,我们就可以通过java语言编写一个javaagent来监控或者操作JVM了,比如对类进行插桩。

Instrumention支持的功能都在java.lang.instrument.Instrumentation接口中体现,而我们最关注的还是其中涉及到类转换相关的方法,比如addTransformer以及retransformClasses

代码语言:javascript
复制
public interface Instrumentation {
    // 添加一个ClassFileTransformer
    // 之后类加载时都会经过这个ClassFileTransformer转换
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

    void addTransformer(ClassFileTransformer transformer);
    // 移除ClassFileTransformer
    boolean removeTransformer(ClassFileTransformer transformer);

    boolean isRetransformClassesSupported();
    // 将一些已经加载过的类重新拿出来经过注册好的ClassFileTransformer转换
    // retransformation可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

    boolean isRedefineClassesSupported();

    // 重新定义某个类
    void redefineClasses(ClassDefinition... definitions)
        throws  ClassNotFoundException, UnmodifiableClassException;

    boolean isModifiableClass(Class<?> theClass);

    @SuppressWarnings("rawtypes")
    Class[] getAllLoadedClasses();

    @SuppressWarnings("rawtypes")
    Class[] getInitiatedClasses(ClassLoader loader);

    long getObjectSize(Object objectToSize);

    void appendToBootstrapClassLoaderSearch(JarFile jarfile);

    void appendToSystemClassLoaderSearch(JarFile jarfile);

    boolean isNativeMethodPrefixSupported();

    void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}

当我们通过addTransformer添加了一个ClassFileTransformer之后,之后所有的类都会通过ClassFileTransformer.transform()方法进行转换,而具体怎么转换,我们可以通过重写transform方法进行自定义,对于已经加载的类,可以通过调用retransformClasses来重新触发这个Transformer的转换,而且Transformer是可以添加多个的,多个transformer会依次执行。

下面,我们来看一下怎么开发一个基于Instrumention的agent吧

开发一个Javaagent

开发一个javaagent需要几步呢?

  1. 创建一个包含premain()方法的类
  2. 创建一个实现ClassFileTransformer接口的Transfromer类
  3. 创建一个MANIFEST.MF文件,且这个文件的Premain-Class配置项必须设置为实现了premain方法的类的类名
  4. 将项目打包成jar包

然后我们就可以通过命令java -javaagent:agent.jar demo.jar来使用我们的javaagent了。

接下来,我们开始写代码,首先创建一个包含premain方法的类,其中premain方法需要严格按照下面两种格式的一种:

代码语言:javascript
复制
//agentArgs是一个字符串,会随着jvm启动设置的参数得到
//inst就是我们需要的Instrumention实例了,由JVM传入。我们可以拿到这个实例后进行各种操作
public static void premain(String agentArgs, Instrumentation inst);  [1]
public static void premain(String agentArgs); [2]

javaagent在执行时会首先查找第一个premain方法,如果找到了就不会执行第二个了,如果没有第一个,才回去执行第二个

其实从premain方法的名字上也可以看出来,这个方法会先于main方法执行,实际上,它会在大多数类加载之前运行,这也是为什么它可以对类进行转换

编写一个Agent类:

代码语言:javascript
复制
public class Agent  
{  
    public static void premain(String agentArgs, Instrumentation inst){  
   inst.addTransformer(new MyClassTransformer(), true);  
 }  
}

其中MyClassTransformer是我自定义的实现了ClassFileTransformer接口的类:

代码语言:javascript
复制
public class MyClassTransformer implements ClassFileTransformer {  
    @Override  
  public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {  
       
   if("org/example/Person".equals(className)){  
            try{  
                ClassPool pool = new ClassPool(true);  
     //pool.insertClassPath("/Users/momo/IdeaProjects/javaagent-demo/tester/target/classes");  
     CtClass clazz = pool.get("org.example.Person");  
     CtMethod method = clazz.getDeclaredMethod("getName");  
     System.out.println(method.getMethodInfo());  
     String source = "{System.out.println(\"hello tntaxin, you are good!\");}";  
     method.setBody(source);  
     byte[] byteCode = clazz.toBytecode();  
     clazz.detach();  
     return byteCode;  
     }catch(Exception e){  
                 e.printStackTrace();  
     }  
        }  
        return new byte[0];  
  }  
}

这个类中就实现了一个transform方法,我借助javaassist的

代码语言:javascript
复制
javassist.ClassPool  
javassist.CtClass
javassist.CtMethod

这三个类对org.example.Person类的getName方法的方法体进行了替换,我们看一下Person类原本的实现:

除了javaassist还可以使用asm对字节码进行修改,后者使用难度相对来说更大一点,但是性能更好,asm入门:https://github.com/dengshiwei/asm-module/blob/master/doc/blog/AOP%20%E5%88%A9%E5%99%A8%20ASM%20%E5%9F%BA%E7%A1%80%E5%85%A5%E9%97%A8.md

代码语言:javascript
复制
public class Person  
{  
    public static void main( String[] args )  
    {  
        Person p = new Person();  
   p.getName();  
  }  
  
    public void getName(){  
        System.out.println("tntaxin");  
  }  
}

可以看到,原本的getName方法会打印tntaxin,而经过agent处理过后的getName应该会打印hello tntaxin, you are good!

接下来我们把javaagent打成jar包验证一下效果,不过,在这之前,不要忘了配置MANIFEST.MF文件

打包完成后,我们在IDEA中配置一下VM Options使用我们刚刚打包好的agent.jar

然后执行Person.main方法,输出如下:

至此,我们已经掌握了简单的javaagent的实现方法,不过上面这种javaagent需要在jvm启动前设置-javaagent参数,但是很多时候,我们想要在程序运行的过程中去插入agent,并修改其中的类。而正好,在Java6的新特性中支持通过attach的方式去加载agent

这种agent又要怎么实现呢?

和之前的agent很像,我们需要创建一个实现以下两种方法中的一种的类

代码语言:javascript
复制
public static void agentmain (String agentArgs, Instrumentation inst); [1] 
public static void agentmain (String agentArgs);[2]

同样的,第一个agentmain方法优先级更高。之后要在META-INF/MAINIFEST.MF属性当中加入” Agent-Class”来指定拥有agentmain方法的类

我们在之前的Agent类基础上添加agentmain方法:

代码语言:javascript
复制
public class Agent  
{  
    public static void premain(String agentArgs, Instrumentation inst){  
        System.out.println("agentArgs: " + agentArgs);  
        inst.addTransformer(new MyClassTransformer(), true);  
  }  
  
    public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException,InterruptedException {  
        System.out.println("agentArgs: " + agentArgs);  
   inst.addTransformer(new MyClassTransformer(), true);  
   // 由于类已经加载完毕,需要执行retransformClasses触发重新加载  
   Class<?> aClass = Class.forName("org.example.Person");  
   inst.retransformClasses(aClass);  
  }  
}

然后打包该agent,之后再编写一个Test类去attach目标进程并加载这个agent

代码语言:javascript
复制
public class Test {  
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException,  
 AgentInitializationException, IOException, AttachNotSupportedException, InterruptedException {  
        //这个pid填写具体要attach的目标进程  
   VirtualMachine attach = VirtualMachine.attach("99812");  
   attach.loadAgent("/Users/xxx/IdeaProjects/javaagent-demo/out/artifacts/agent_jar/agent.jar");  
   attach.detach();  
   System.out.println("over");  
  }  
}

最后修改一下之前的Person类,确保它一直运行着:

代码语言:javascript
复制
public class Person  
{  
    public static void main( String[] args ) throws InterruptedException {  
        Person p = new Person();  
   // 获取当前进程id  
   RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();  
   String name = runtime.getName();  
   String pid = name.substring(0, name.indexOf('@' ));  
   System.out.println(pid);  
   while(true){  
   Thread.sleep(3000);  
    p.getName();  
   }  
    }  
  
    public void getName(){  
        System.out.println("tntaxin");  
  }  
}

接下来我们看下效果,先运行Person类,然后再运行Test类:

在没运行Test类之前一直输出着tntaxin,运行Test类将agent附加到进程后,输出内容变成了hello tntaxin, you are good!

其他

在写这个demo的过程中遇到了一个错误:

Agent JAR loaded but agent failed to initialize

查资料发现是因为我的agent因为发生异常没有detach,导致我后面再次加载agent时和之前的agent冲突了,因为已经加载过了嘛,解决方案是修改Agent的类以及jar包名,然后重新加载,这样就不会冲突了。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-11-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一个安全研究员 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • JVMTI
  • Instrumention 机制
  • 开发一个Javaagent
  • 其他
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档