JVM基礎知識和ASM修改字節碼

本文會介紹一下ASM的簡單使用和一些JVM相關的知識,但是不會很詳細的涵蓋所有內容。

爲了方便理解,我會分別介紹以下內容

  1. JVM基礎知識
  2. Java字節碼基礎知識
  3. ASM基礎使用

JVM 基礎知識

因爲字節碼中的指令執行和JVM相關,所以需要先介紹一下JVM基礎知識。

JVM 虛擬機棧

Java稍有了解的開發人員,應該都知道JVM有一個Java虛擬機棧,棧中的每一個元素被稱爲Frame(棧幀),你可以簡單的理解一個Java方法和一個Frame對應。

https://img-blog.csdnimg.cn/20190304112913334.PNG

Java某個方法被執行時,首先方法對應的frame先入棧,也就是說棧頂的frame會對應當前正常執行的方法;當一個方法執行完成後,frame出棧。

Frame

Frame操作數棧局部變量表組成。

操作數棧

和虛擬機棧類似,操作數棧每個元素表示一個jvm指令。在彙編代碼中,我們會把一些需要被指令使用的值,放在寄存器中,而在JVM裏這一點有所不同。JVM指令執行過程中,會把需要使用到的值放在操作操作數棧內。如果某個指令需要使用n個值,就會從棧頂開始取n個值,然後把執行結果放入操作數棧頂。

https://img-blog.csdnimg.cn/20190304122847523.PNG?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3N3ZWF0T3R0,size_16,color_FFFFFF,t_70

局部變量表

操作數棧在執行指令時,我們可能需要把結果佔時賦予某個變量,等待其他指令使用它。這時候就需要使用到局部變量表,通過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://img-blog.csdnimg.cn/20190304105559949.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3N3ZWF0T3R0,size_16,color_FFFFFF,t_70

  1. boolean 在字節碼中用Z表示
  2. charC表示
  3. …省略(直接看圖吧)
  4. 對應在字節碼中需要以L開頭,後面爲類全路徑名,最後必須以;結尾
  5. 數組需要以[開頭

字節碼中的 Java 方法

https://img-blog.csdnimg.cn/2019030411002539.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3N3ZWF0T3R0,size_16,color_FFFFFF,t_70

表格中第一個方法void m(int i, float f)在字節碼中爲(IF)V,括號裏的內容爲方法的參數int i使用I表示,忽略參數名稱,float使用F表示。括號後面的值表示方法返回值,由於此方法沒有返回值(void),使用V表示void

其它幾個方法通過上一小節和前面的介紹,可以很容易理解。

字節碼基礎指令

本小節比較枯燥,可以先跳過,遇到後回來查指令功能。

XLoad表示將不同類型的值放入操作數棧頂。

https://img-blog.csdnimg.cn/20190304121136300.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3N3ZWF0T3R0,size_16,color_FFFFFF,t_70

Stack爲操作數棧元素的操作相關的指令。

Constants爲將常數放入到操作數棧的相關指令。

Arithmetic and logic爲一些運算相關的指令。

Casts爲強制轉換的相關指令。

Objects爲對應創建相關的指令。

https://img-blog.csdnimg.cn/20190304121450549.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3N3ZWF0T3R0,size_16,color_FFFFFF,t_70

Methods爲調用方法相關的指令。

Arrays爲數組相關的指令。

https://img-blog.csdnimg.cn/20190304121941123.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3N3ZWF0T3R0,size_16,color_FFFFFF,t_70

到這裏介紹了一些Java字節碼相關的基礎知識,可能並沒有很全。

ASM 修改字節碼

本節將會介紹ASM的使用方式。

ASM框架提供了3個比較特殊的類

  1. ClassReader; 能夠解析class文件,轉化爲二進制數組,對於class的方法,註解,參數等等內容,會作爲ClassVisitorvisitXxx方法參數,提供給ClassVisitor類。
  2. ClassWriter; ClassVisitor子類,能夠將二進制數組轉化爲編譯後的class文件。
  3. ClassVisitor; 對一個類的描述抽象,可以委派visitXxx方法給另一個ClassVisitor

工作方式如下圖,其中ClassVisitor可以由多個組合而成。

https://img-blog.csdnimg.cn/20190304142038476.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寫起來和彙編類似,其實好像也沒有那麼難吧。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章