本文会介绍一下ASM的简单使用和一些JVM相关的知识,但是不会很详细的涵盖所有内容。

为了方便理解,我会分别介绍以下内容

  1. JVM基础知识
  2. Java字节码基础知识
  3. ASM基础使用

JVM 基础知识

因为字节码中的指令执行和JVM相关,所以需要先介绍一下JVM基础知识。

JVM 虚拟机栈

Java稍有了解的开发人员,应该都知道JVM有一个Java虚拟机栈,栈中的每一个元素被称为Frame(栈帧),你可以简单的理解一个Java方法和一个Frame对应。

https://i-blog.csdnimg.cn/blog_migrate/45fc8d37842a6e62971d1364a3cc9085.png

Java某个方法被执行时,首先方法对应的frame先入栈,也就是说栈顶的frame会对应当前正常执行的方法;当一个方法执行完成后,frame出栈。

Frame

Frame操作数栈局部变量表组成。

操作数栈

和虚拟机栈类似,操作数栈每个元素表示一个jvm指令。在汇编代码中,我们会把一些需要被指令使用的值,放在寄存器中,而在JVM里这一点有所不同。JVM指令执行过程中,会把需要使用到的值放在操作操作数栈内。如果某个指令需要使用n个值,就会从栈顶开始取n个值,然后把执行结果放入操作数栈顶。

https://i-blog.csdnimg.cn/blog_migrate/bab2063d321e5c2e9737a54b22ec1098.png

局部变量表

操作数栈在执行指令时,我们可能需要把结果占时赋予某个变量,等待其他指令使用它。这时候就需要使用到局部变量表,通过Xstore指令(X表示不同类型有不同的store指令),将操作数栈顶元素赋予给指定的局部变量。

接下来我们通过一个java方法,来学习操作数栈和局部变量表的工作方式。

public void hello() {
    int a = 1;
    int b = 2;
    int c = a + b;
    System.out.println(c);
}

方法hello非常简单,然后通过idea插件ASM Bytecode Outline获取它的字节码

  public hello()V
   L0
    ICONST_1
    ISTORE 1
   L1
    ICONST_2
    ISTORE 2
   L2
    ILOAD 1
    ILOAD 2
    IADD
    ISTORE 3
   L3
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ILOAD 3
    INVOKEVIRTUAL java/io/PrintStream.println (I)V
   L4
    RETURN

首先我们对于 int a = 1int b = 2,它们对应的字节码为上面的Label L0L1

L0字节码做了什么?

ICONST表示将int类型的值1放到操作数栈顶,随后使用ISTORE将操作数栈顶的int类型值(也就是我们通过ICONST放入栈顶的值1)放入到局部变量表索引1对应的局部变量。L1的字节码和L0功能类似。

对于int c = a + b,我们可以看L2

ILOAD 1ILOAD 2表示,将索引1和索引2对应的局部变量,放入操作数栈(按照指令执行顺序入栈)。使用IADD指令,从操作数栈顶取2个值相加,最后将结果放回操作数栈顶。(这些指令都已I开头,表示int类型的值) 最后使用ISTORE 3将操作数栈顶的值放到索引3对应的局部变量(对应java代码将值赋予c)。

System.out.println(c);

c输出到控制台的代码对应L3GETSTATIC java/lang/System.out : Ljava/io/PrintStream;表示调用java/lang/System.out静态方法,它的返回值是Ljava/io/PrintStream类型(字节码中的JAVA类型可以参考下一节),返回的结果会放入操作数栈顶。

ILOAD 3表示将索引3对应的局部变量放入操作数栈顶,最后执行INVOKEVIRTUAL java/io/PrintStream.println (I)V 指令,表示调用println方法,后面的(I)V是对方法的描述,表示需要一个int类型参数,返回为V。具体可以参考下一节。

由于每个frame的工作和栈相关,如果线程不安全,会导致frame入栈出栈错误,所以Java虚拟机栈必须是线程安全的,也就是说是Java虚拟机栈是线程私有的。到这里应该已经能够理解Java虚拟机栈的工作方式了。

Java 字节码基础知识

本节内容会非常的基础,以最快的方式,学习字节码的基础常识,为之后内容做铺垫。如果不想看这些烦人的描述,可以先跳过,当然最后你又会回来的 ,因为这些是必不可少的知识点。

字节码中的 Java 类型

https://i-blog.csdnimg.cn/blog_migrate/7c82c5a254828acb3cfa4d8789c2a8e3.png

  1. boolean 在字节码中用Z表示
  2. charC表示
  3. …省略(直接看图吧)
  4. 对应在字节码中需要以L开头,后面为类全路径名,最后必须以;结尾
  5. 数组需要以[开头

字节码中的 Java 方法

https://i-blog.csdnimg.cn/blog_migrate/e030a151e62e5224dea39b1edb4df724.png

表格中第一个方法void m(int i, float f)在字节码中为(IF)V,括号里的内容为方法的参数int i使用I表示,忽略参数名称,float使用F表示。括号后面的值表示方法返回值,由于此方法没有返回值(void),使用V表示void

其它几个方法通过上一小节和前面的介绍,可以很容易理解。

字节码基础指令

本小节比较枯燥,可以先跳过,遇到后回来查指令功能。

XLoad表示将不同类型的值放入操作数栈顶。

https://i-blog.csdnimg.cn/blog_migrate/9592db355bc1e2502ee91c9883bdbe8e.png

Stack为操作数栈元素的操作相关的指令。

Constants为将常数放入到操作数栈的相关指令。

Arithmetic and logic为一些运算相关的指令。

Casts为强制转换的相关指令。

Objects为对应创建相关的指令。

https://i-blog.csdnimg.cn/blog_migrate/fc9e60f271a2b7dbf023b6cd39b475a0.png

Methods为调用方法相关的指令。

Arrays为数组相关的指令。

https://i-blog.csdnimg.cn/blog_migrate/3ee5841f9f7384fa4dae8c2a4efe0e9c.png

到这里介绍了一些Java字节码相关的基础知识,可能并没有很全。

ASM 修改字节码

本节将会介绍ASM的使用方式。

ASM框架提供了3个比较特殊的类

  1. ClassReader; 能够解析class文件,转化为二进制数组,对于class的方法,注解,参数等等内容,会作为ClassVisitorvisitXxx方法参数,提供给ClassVisitor类。
  2. ClassWriter; ClassVisitor子类,能够将二进制数组转化为编译后的class文件。
  3. ClassVisitor; 对一个类的描述抽象,可以委派visitXxx方法给另一个ClassVisitor

工作方式如下图,其中ClassVisitor可以由多个组合而成。

https://i-blog.csdnimg.cn/blog_migrate/3cc8f8321294e9ec7fb5af36c325d887.png

ClassVisitor内部有多个visitXxx方法,分别用于解析class的不同结构。开人同学可以继承ClassVisitor,实现自己的visitXxx方法,做到修改class内容,最后通过ClassWriter生成新的.class文件。

public abstract class ClassVisitor {
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces);
    public void visitSource(String source, String debug);
    public void visitOuterClass(String owner, String name, String desc);
    public AnnotationVisitor visitAnnotation(String desc, boolean visible);
    public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String desc, boolean visible);
    public void visitAttribute(Attribute attr);
    public void visitInnerClass(String name, String outerName, String innerName, int access);
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value);
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions);
    public void visitEnd();
}

对于如何生成.class文件本文不涉及,因为网上有很多文章介绍如何生成.class文件,可以自己搜搜。

ASM实操

本小节实际使用ASM修改类的内容。项目repo https://github.com/sweat123/ASM-demo

public class Hello {
    @Add
    public int add(int x, int y) {
        return x + y;
    }
}

通过ASM,如果检测到方法上有@Add注解,则修改add方法内容如下

public class Hello {
    public int add(int x, int y) {
        int a = x + y;
        return a;
    }
}
maven
<dependencies>
    <dependency>
        <groupId>org.ow2.asm</groupId>
        <artifactId>asm</artifactId>
        <version>5.1</version>
    </dependency>
    <dependency>
        <groupId>org.ow2.asm</groupId>
        <artifactId>asm-commons</artifactId>
        <version>5.1</version>
    </dependency>
</dependencies>
定义 Add 注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Add {
}
定义 ClassVisitor

ClassVisitor里面重写visitMethod方法,由于java对象初始化时,有一个<init>方法,由JVM添加,需要忽略它。

public class AddClassVisitor extends ClassVisitor {

    public AddClassVisitor(final ClassVisitor cv) {
        super(ASM5, cv);
    }

    @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 (!name.equals("<init>")) {
            return new AddMethodVisitor(mv, access, name, desc);
        }
        return mv;
    }
}
定义 MethodVisitor

每个方法对应一个MethodVisitor实例。在MethodVisitor里将会修改方法内容。AdviceAdapterasm-commons里面的一个类,它帮助我们简化了Asm很多负责操作。它也是MethodVisitor的自类。

  1. 首先我们重写visitAnnotation方法,判断某个方法是否有@Add注解。如果有,表示当前方法需要被修改。
  2. 重写visitCode方法,如果存在@Add注解,我们通过ASM实现add()方法的内容。

mv.visitVarInsn(Opcodes.ILOAD, 1);表示将指向局部变量表索引为1的值放入操作数栈。局部变量表的0索引表示this1~n前几个指向方法的参数。也就是说1, 2索引指向方法addx, y参数。

int newLocal = newLocal(Type.INT_TYPE);表示创建一个新的局部变量索引,存放的值类型为int

mv.visitVarInsn(Opcodes.ISTORE, newLocal);将结果放入新创建的索引指向的位置,对应java代码a = x + y

mv.visitInsn(Opcodes.IRETURN);表示将操作数栈顶值返回给上一个方法。

public class AddMethodVisitor extends AdviceAdapter {

    private boolean addAnnotation = false;

    protected AddMethodVisitor(final MethodVisitor mv, final int access, final String name, final String desc) {
        super(ASM5, mv, access, name, desc);
    }
    @Override
    public AnnotationVisitor visitAnnotation(final String desc, final boolean visible) {
        if (visible && desc.equals("Lcom/github/laomei/asm/Add;")) {
            addAnnotation = true;
        }
        return super.visitAnnotation(desc, visible);
    }
    @Override
    public void visitCode() {
        super.visitCode();
        if (!addAnnotation) {
            super.visitEnd();
            return;
        }
        # 将 x, y放入操作数栈
        mv.visitVarInsn(Opcodes.ILOAD, 1);
        mv.visitVarInsn(Opcodes.ILOAD, 2);
        # 栈顶2个元素相加,将结果放回栈顶
        mv.visitInsn(Opcodes.IADD);
        # 创建新的局部变量表索引
        int newLocal = newLocal(Type.INT_TYPE);
        # 将操作数栈顶值放入创建的索引指向的位置
        mv.visitVarInsn(Opcodes.ISTORE, newLocal);
        # 将结果放回操作数栈顶
        mv.visitVarInsn(Opcodes.ILOAD, newLocal);
        # 返回操作数栈顶值
        mv.visitInsn(Opcodes.IRETURN);
    }
}
执行
public class Main {
    public static void main(String[] args)
            throws IOException {
        ClassReader classReader = new ClassReader("com/github/laomei/asm/Hello");
        ClassWriter classWriter = new ClassWriter(COMPUTE_MAXS);
        AddClassVisitor metricClassVisitor = new AddClassVisitor(classWriter);
        classReader.accept(metricClassVisitor, ClassReader.SKIP_FRAMES);
        byte[] bytes = classWriter.toByteArray();
        File file = new File("Hello.class");
        file.createNewFile();
        FileUtils.writeByteArrayToFile(file, bytes);
    }
}
结果

新生成的Hello.class文件内容,可以看到成功修改了方法内容。

package com.github.laomei.asm;

public class Hello {
    public Hello() {
    }

    @Add
    public int add(int var1, int var2) {
        int var3 = var1 + var2;
        return var3;
    }
}

通过上面的实操,应该算是ASM入门了吧~~

总结

ASM写起来和汇编类似,其实好像也没有那么难吧。。。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐