本文會介紹一下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
寫起來和彙編類似,其實好像也沒有那麼難吧。。。