阿尔萨斯 原理探究 asm再认识(转载)

前言

在前面博客中,我们通过写好的 Java 文件再编译成 class 文件来替换目标类,这样存在着一定的局限性。而使用 asm 可以直接对 class 文件进行读写,本文将演示如何通过ASM Bytecode Outline 插件对 class 文件进行加强。以下内容转载自:手摸手增加字节码往方法体内插代码

手摸手增加字节码往方法体内插代码

话不多说,先看本次想实现怎样的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static class Bazhang {

private long f(int n, String s, int[] arr) {
return 0;
}

private void hi(double a, List<String> b) {

}

public void newFunc(String str) {
System.out.println(str);
for (int i = 0; i < 100; i++) {
if (i % 10 == 0) {
System.out.println(i);
}
}
}
}

这是一个自定义的类,里面有三个方法,我需要在不改变原有写好的代码的基础上,往newFunc(String str)这个方法内收尾增加两个方法,打印输入start和end,也就是如下:

1
2
3
4
5
6
7
8
9
10
public static void newFunc(String str) {
System.out.println("========start=========");
System.out.println(str);
for (int i = 0; i < 100; i++) {
if (i % 10 == 0) {
System.out.println(i);
}
}
System.out.println("========end=========");
}

那么我们直接操作字节码,往方法体内首尾增加相应字节码就好了。

这里先安利一个IntelliJ的插件,叫做ASM Bytecode Outline,他可以直接显示java代码对应的字节码和ASM相应的操作代码,这个插件一定程度上帮助我们写接下来的代码。

下面进入正题,手摸手开始:

自定义一个ClassVisitor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static class TestClassVisitor extends ClassVisitor {
public TestClassVisitor(final ClassVisitor cv) {
super(Opcodes.ASM5, cv);
}

@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
if (cv != null) {
cv.visit(version, access, name, signature, superName, interfaces);
}
}

@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
//如果methodName是newFunc,则返回我们自定义的TestMethodVisitor
if ("newFunc".equals(name)) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
return new TestMethodVisitor(mv);
}
if (cv != null) {
return cv.visitMethod(access, name, desc, signature, exceptions);
}

return null;
}
}

确保只有newFunc方法才会走我们的套路。

自定义TestMethodVisitor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static class TestMethodVisitor extends MethodVisitor {

public TestMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM5, mv);
}

@Override
public void visitCode() {
//方法体内开始时调用
super.visitCode();
}

@Override
public void visitInsn(int opcode) {
//每执行一个指令都会调用
super.visitInsn(opcode);
}
}

我们注释了两个方法,一个是visitCode(),一个是visitInsn(int opcode),这两个方法一个为我们接下来插入start,一个插入end。

使用ASM Bytecode Outline插件做简单分析

这一步并不是必须的,如果你对字节码足够的熟练,完全可以随便撸。

先随便写个类,然后实现我们最后需要变成的newFunc(String str)方法,然后用插件查看ASMifield,可以得到如下片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("========start=========");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

...

// 2
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

...

// 3
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("========end=========");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

...

// 4
mv.visitInsn(RETURN);

我把中间很多代码给删掉了,这样结构就很清楚了,可以看到我留下了四个部分:

注释1:这是我们要插入的System.out.println(“========start=========”);转成相对应的ASM提供的方法,其中visitLdcInsn可以在JVM指令表中查到Ldc表示将int, float或String型常量值从常量池中推送至栈顶,那么其实ASM提供了方法让我们继续通过java代码转成相对应的字节码,那么注释1中的三行代码所对应的字节码就是:

1
2
3
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "========start========="
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V

注释2:这是原方法内的代码System.out.println(str);,表明我们注释1确实是插在了newFunc方法体的最上方,其中mv.visitVarInsn(ALOAD, 0);的ALOAD对应JVM指令的意思是将指定的引用类型本地变量推送至栈顶,因为这个String是参数传过来的。

注释3:显而易见,转换前的java代码就是我们的System.out.println(“========end=========”);,这里不再赘述。

注释4:RETURN对应着从当前方法返回void

这样一来,ASM Bytecode Outline插件帮我们直接生成了相对应的ASM代码,那么我们接下来粘贴复制就行了。

插头

先从简单的开始,插头部。我们在之前的自定义TestMethodVisitor中已经复写了visitCode方法,那么我们就在代码注释的地方插入ASM代码:

1
2
3
4
5
6
7
@Override
public void visitCode() {
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("========start=========");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
super.visitCode();
}

插尾

这个比插头复杂点,但是也很简单,visitInsn方法会在每个指令被执行时都会调用,所以我们需要判断指令是否到了RETURN即可,在RETURN前插入我们的代码:

1
2
3
4
5
if (opcode == Opcodes.RETURN) {
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("========end=========");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}

校验

这样一来,我们的头尾的插好了,校验一番:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) throws Exception {
ClassReader cr = new ClassReader(Bazhang.class.getName());
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
ClassVisitor cv = new TestClassVisitor(cw);

cr.accept(cv, Opcodes.ASM5);

// 获取生成的class文件对应的二进制流
byte[] code = cw.toByteArray();

//将二进制流写到out/下
FileOutputStream fos = new FileOutputStream("out/Bazhang223.class");
fos.write(code);
fos.close();
}

可以看到如下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.List;

public class Test$Bazhang {
public Test$Bazhang() {
}

private long f(int n, String s, int[] arr) {
return 0L;
}

private void hi(double a, List<String> b) {
}

public void newFunc(String str) {
System.out.println("========start=========");
System.out.println(str);

for(int i = 0; i < 100; ++i) {
if(i % 10 == 0) {
System.out.println(i);
}
}

System.out.println("========end=========");
}
}

看来导出的.class是没问题了,那么利用反射来执行一下我们的修改类:

1
2
3
4
5
Test loader = new Test();
Class hw = loader.defineClass("Test$Bazhang", code, 0, code.length);
Object o = hw.newInstance();
Method method = o.getClass().getMethod("newFunc", String.class);
method.invoke(o, "巴掌菜比");

最后控制台打印出来的结果是:

1
2
3
4
5
6
7
8
9
10
11
12
13
========start=========
巴掌菜比
0
10
20
30
40
50
60
70
80
90
========end=========

尾语

这样一来我们的目的都达到了,之后便可以更加进一步做点有意思的事,比如统计方法耗时。

整的来说,修改字节码达到本次想要的效果是个很cool的方式,很多时候我们是可以通过hook或者动态代理来做一些类似本文的操作,那么就要结合实际情况进行选择了。