Java 动态字节码技术(2)

JVM TI(JVM Tool Interface)JVM 工具接口是 JVM 提供的一个非常强大的对 JVM 操作的工具接口,通过这个接口,我们可以实现对 JVM 多种组件的操作,从JVMTM Tool Interface 这里我们认识到 JVM TI 的强大,它包括了对虚拟机堆内存、类、线程等各个方面的管理接口。

JVM TI 通过事件机制,通过接口注册各种事件勾子,在 JVM 事件触发时同时触发预定义的勾子,以实现对各个 JVM 事件的感知和反应。

Agent

Agent 是 JVM TI 实现的一种方式。我们在编译 C 项目里链接静态库,将静态库的功能注入到项目里,从而才可以在项目里引用库里的函数。我们可以将 agent 类比为 C 里的静态库,我们也可以用 C 或 C++ 来实现,将其编译为 dll 或 so 文件,在启动 JVM 时启动。

这时再来思考 Debug 的实现,我们在启动被 Debug 的 JVM 时,必须添加参数 -agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:3333,而 -agentlib 选项就指定了我们要加载的 Java Agent,jdwp 是 agent 的名字,在 linux 系统中,我们可以在 jre 目录下找到 jdwp.so 库文件。

Java 的调试体系 jdpa 组成,从高到低分别为 jdi->jdwp->jvmti,我们通过 JDI 接口发送调试指令,而 jdwp 就相当于一个通道,帮我们翻译 JDI 指令到 JVM TI,最底层的 JVM TI 最终实现对 JVM 的操作。

使用

JVM TI 的 agent 使用很简单,在启动 agent 时添加 -agent 参数指定我们要加载的 agent jar包即可。

而要实现代码的修改,我们需要实现一个 instrument agent,它可以通过在一个类里添加 premain() 或 agentmain() 方法来实现。而要实现 1.6 以上的动态 instrument 功能,实现 agentmain 方法即可。

在 agentmain 方法里,我们调用 Instrumentation.retransformClasses() 方法实现对目标类的重定义。

另外往一个正在运行的 JVM 里动态添加 agent,还需要用到 JVM 的 attach 功能,Sun 公司的 tools.jar 包里包含的 VirtualMachine 类提供了 attach 一个本地 JVM 的功能,它需要我们传入一个本地 JVM 的 pid, tools.jar 可以在 jre 目录下找到。

agent生成

另外,我们还需要注意 agent 的打包,它需要指定一个 Agent-Class 参数指定我们的包括 agentmain 方法的类,可以算是指定入口类吧。

此外,还需要配置 MANIFEST.MF 文件的一些参数,允许我们重新定义类。如果你的 agent 实现还需要引用一些其他类库时,还需要将这些类库都打包到此 jar 包中,下面是我的 pom 文件配置。

<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <configuration> <archive> <manifestEntries> <Agent-Class>asm.TestAgent</Agent-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> <Manifest-Version>1.0</Manifest-Version> <Permissions>all-permissions</Permissions> </manifestEntries> </archive> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> </plugin> </plugins> </build>

另外在打包时需要使用 mvn assembly:assembl 命令生成 jar-with-dependencies 作为 agent。

代码实现

我在测试时写了一个用以上技术实现了一个简单的字节码动态修改的 Demo。

被修改的类

TransformTarget 是要被修改的目标类,正常执行时,它会三秒输出一次 “hello”。

public class TransformTarget { public static void main(String[] args) { while (true) { try { Thread.sleep(3000L); } catch (Exception e) { break; } printSomething(); } } public static void printSomething() { System.out.println("hello"); } }

Agent

Agent 是执行修改类的主体,它使用 ASM 修改 TransformTarget 类的方法,并使用 instrument 包将修改提交给 JVM。

入口类,也是代理的 Agent-Class。

public class TestAgent { public static void agentmain(String args, Instrumentation inst) { inst.addTransformer(new TestTransformer(), true); try { inst.retransformClasses(TransformTarget.class); System.out.println("Agent Load Done."); } catch (Exception e) { System.out.println("agent load failed!"); } } }

执行字节码修改和转换的类。

public class TestTransformer implements ClassFileTransformer { public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { System.out.println("Transforming " + className); ClassReader reader = new ClassReader(classfileBuffer); ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES); ClassVisitor classVisitor = new TestClassVisitor(Opcodes.ASM5, classWriter); reader.accept(classVisitor, ClassReader.SKIP_DEBUG); return classWriter.toByteArray(); } class TestClassVisitor extends ClassVisitor implements Opcodes { TestClassVisitor(int api, ClassVisitor classVisitor) { super(api, classVisitor); } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); if (name.equals("printSomething")) { mv.visitCode(); Label l0 = new Label(); mv.visitLabel(l0); mv.visitLineNumber(19, l0); mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn("bytecode replaced!"); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); Label l1 = new Label(); mv.visitLabel(l1); mv.visitLineNumber(20, l1); mv.visitInsn(Opcodes.RETURN); mv.visitMaxs(2, 0); mv.visitEnd(); TransformTarget.printSomething(); } return mv; } } }

Attacher

使用 tools.jar 里方法将 agent 动态加载到目标 JVM 的类。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/0f53060fe9ab25a89424a3e1181c6329.html