本文会介绍一下ASM
的简单使用和一些JVM
相关的知识,但是不会很详细的涵盖所有内容。
为了方便理解,我会分别介绍以下内容
- JVM基础知识
- Java字节码基础知识
- ASM基础使用
JVM 基础知识
因为字节码中的指令执行和JVM
相关,所以需要先介绍一下JVM
基础知识。
JVM 虚拟机栈
对Java
稍有了解的开发人员,应该都知道JVM有一个Java
虚拟机栈,栈中的每一个元素被称为Frame
(栈帧),你可以简单的理解一个Java
方法和一个Frame
对应。
Java
某个方法被执行时,首先方法对应的frame
先入栈,也就是说栈顶的frame
会对应当前正常执行的方法;当一个方法执行完成后,frame
出栈。
Frame
Frame
由操作数栈
和局部变量表
组成。
操作数栈
和虚拟机栈类似,操作数栈每个元素表示一个jvm
指令。在汇编代码中,我们会把一些需要被指令使用的值,放在寄存器中,而在JVM
里这一点有所不同。JVM
指令执行过程中,会把需要使用到的值放在操作操作数栈内。如果某个指令需要使用n
个值,就会从栈顶开始取n
个值,然后把执行结果放入操作数栈顶。
局部变量表
操作数栈在执行指令时,我们可能需要把结果占时赋予某个变量,等待其他指令使用它。这时候就需要使用到局部变量表,通过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 = 1
和int b = 2
,它们对应的字节码为上面的Label L0
和L1
。
L0
字节码做了什么?
ICONST
表示将int
类型的值1
放到操作数栈顶,随后使用ISTORE
将操作数栈顶的int
类型值(也就是我们通过ICONST
放入栈顶的值1
)放入到局部变量表索引1
对应的局部变量。L1
的字节码和L0
功能类似。
对于int c = a + b
,我们可以看L2
。
ILOAD 1
和ILOAD 2
表示,将索引1
和索引2
对应的局部变量,放入操作数栈(按照指令执行顺序入栈)。使用IADD
指令,从操作数栈顶取2
个值相加,最后将结果放回操作数栈顶。(这些指令都已I
开头,表示int
类型的值) 最后使用ISTORE 3
将操作数栈顶的值放到索引3
对应的局部变量(对应java
代码将值赋予c
)。
System.out.println(c);
将c
输出到控制台的代码对应L3
,GETSTATIC 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 类型
boolean
在字节码中用Z
表示char
用C
表示- …省略(直接看图吧)
- 对应在字节码中需要以
L
开头,后面为类全路径名,最后必须以;
结尾 - 数组需要以
[
开头
字节码中的 Java 方法
表格中第一个方法void m(int i, float f)
在字节码中为(IF)V
,括号里的内容为方法的参数int i
使用I
表示,忽略参数名称,float
使用F
表示。括号后面的值表示方法返回值,由于此方法没有返回值(void
),使用V
表示void
。
其它几个方法通过上一小节和前面的介绍,可以很容易理解。
字节码基础指令
本小节比较枯燥,可以先跳过,遇到后回来查指令功能。
XLoad
表示将不同类型的值放入操作数栈顶。
Stack
为操作数栈元素的操作相关的指令。
Constants
为将常数放入到操作数栈的相关指令。
Arithmetic and logic
为一些运算相关的指令。
Casts
为强制转换的相关指令。
Objects
为对应创建相关的指令。
Methods
为调用方法相关的指令。
Arrays
为数组相关的指令。
到这里介绍了一些Java
字节码相关的基础知识,可能并没有很全。
ASM 修改字节码
本节将会介绍ASM
的使用方式。
ASM
框架提供了3
个比较特殊的类
ClassReader
; 能够解析class
文件,转化为二进制数组,对于class
的方法,注解,参数等等内容,会作为ClassVisitor
的visitXxx
方法参数,提供给ClassVisitor
类。ClassWriter
;ClassVisitor
子类,能够将二进制数组转化为编译后的class
文件。ClassVisitor
; 对一个类的描述抽象,可以委派visitXxx
方法给另一个ClassVisitor
。
工作方式如下图,其中ClassVisitor
可以由多个组合而成。
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
里将会修改方法内容。AdviceAdapter
是asm-commons
里面的一个类,它帮助我们简化了Asm
很多负责操作。它也是MethodVisitor
的自类。
- 首先我们重写
visitAnnotation
方法,判断某个方法是否有@Add
注解。如果有,表示当前方法需要被修改。 - 重写
visitCode
方法,如果存在@Add
注解,我们通过ASM
实现add()
方法的内容。
mv.visitVarInsn(Opcodes.ILOAD, 1);
表示将指向局部变量表索引为1
的值放入操作数栈。局部变量表的0
索引表示this
,1~n
前几个指向方法的参数。也就是说1
, 2
索引指向方法add
的x, 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
写起来和汇编类似,其实好像也没有那么难吧。。。