1. 程式人生 > 實用技巧 >asm操作位元組碼,刪除類的成員變數

asm操作位元組碼,刪除類的成員變數

https://blog.csdn.net/fyyyr/article/details/102816064

ASM基礎

ASM是一個Java位元組碼操作框架,可用於class檔案的修改。
其原理是將class檔案載入,然後構建成一棵樹。然後根據使用者自定義的修改類對該樹進行加工,加工完成後即可得到修改後的class檔案。
故而ASM中使用了visitor模式:class檔案的結構是固定的,根據其構造出的樹作為被訪問者,則其節點也是固定的。只需要對每個節點定義一個訪問者即可進行指定的修改。
由於修改class主要涉及欄位和方法,故最常用的visitor是FieldVisitorMethodVisitor
FieldVisitor

為例,當ASM使用FieldVisitor來處理一個class的樹時,則該class的每個方法都會被傳入定義的FieldVisitor。於是只需要在FieldVisitor對特定的方法進行過濾處理即可。
ASM的官方文件地址為:

https://asm.ow2.io/javadoc/overview-summary.html

開啟可以看到其類庫的內容:

其第一個包org.objectweb.asm為核心core包,包含了主要的功能介面和物件。

環境搭建

ASM使用的是com.sun.xml.internal.ws.org.objectweb.asm,因此不需要額外引入庫檔案。
建立一個JBoss工程,載入class並修改,然後將修改後的class檔案進行儲存:

import com.sun.xml.internal.ws.org.objectweb.asm.ClassAdapter;
import com.sun.xml.internal.ws.org.objectweb.asm.ClassReader;
import com.sun.xml.internal.ws.org.objectweb.asm.ClassWriter;
import java.io.*;

public class Main {
    public static void main(String[] args) throws Exception {
        // 載入class檔案
        FileInputStream fis = new FileInputStream("D:\\Test\\Hello.class");
        // 修改
        ClassReader cr = new ClassReader(fis);
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        ClassAdapter classAdapter = new ASMTest(cw);
        cr.accept(classAdapter, ClassReader.SKIP_DEBUG);
        // 儲存class檔案
        FileOutputStream fos = new FileOutputStream("D:\\Test\\Change\\Hello.class");
        fos.write(cw.toByteArray());
        fos.close();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

這就是總體的框架。至於修改,由ASMTest類負責。
新建一個Java類ASMTest

import com.sun.xml.internal.ws.org.objectweb.asm.*;

public class ASMTest extends ClassAdapter {

    public ASMTest(ClassVisitor cv) {
        super(cv);
    }

    // 欄位處理
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        return this.cv.visitField(access, name, desc, signature, value);
    }

    // 方法處理
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        return this.cv.visitMethod(access, name, desc, signature, exceptions);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

上面定義了負責進行修改的類ASMTest。直接執行main(),會在目標路徑下生成一個class。由於ASMTest沒有進行任何修改,故而生成的class與原始class內容相同。

解析

ClassReader

ClassReader能夠處理class檔案的位元組碼資料,構建出一棵該類的抽象樹。然後執行傳入的引數物件所包含的操作,從而對抽象樹進行加工。

ClassAdapter

ClassAdapter繼承自ClassVisitor,負責對class樹進行修改,開發者需要對其繼承並重載對應的修改方法。
也就是說,所有的visitor都整合在了ClassAdapter的方法中,只需要順序呼叫ClassAdapter的所有方法,即可實現所有visitor順序訪問class樹。
ClassAdapter的定義為:

public class ClassAdapter implements ClassVisitor {
    protected ClassVisitor cv;

    public ClassAdapter(ClassVisitor cv) {
        this.cv = cv;
    }

    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        this.cv.visit(version, access, name, signature, superName, interfaces);
    }

    public void visitSource(String source, String debug) {
        this.cv.visitSource(source, debug);
    }

    public void visitOuterClass(String owner, String name, String desc) {
        this.cv.visitOuterClass(owner, name, desc);
    }

    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        return this.cv.visitAnnotation(desc, visible);
    }

    public void visitAttribute(Attribute attr) {
        this.cv.visitAttribute(attr);
    }

    public void visitInnerClass(String name, String outerName, String innerName, int access) {
        this.cv.visitInnerClass(name, outerName, innerName, access);
    }

    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        return this.cv.visitField(access, name, desc, signature, value);
    }

    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        return this.cv.visitMethod(access, name, desc, signature, exceptions);
    }

    public void visitEnd() {
        this.cv.visitEnd();
    }
}
  • 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
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

其所有方法都可被過載。前面的例子中,ASMTest過載了visitField()visitMethod(),從而對欄位和方法進行修改。
ClassAdapter的所有方法是按已安排好的順序來呼叫的,也就是ClassAdapter所有方法的定義順序。這一點通常不需要開發者關心。
關於每個方法的具體說明,可參考其父類ClassVisitor的文件:

https://asm.ow2.io/javadoc/org/objectweb/asm/ClassVisitor.html

修改

以前面ASMTest為例。
設Hello.class的實現為:

public class Hello {
    String words = "Hello world!";
    int value = 1;

    public void say() {
        System.out.println(words);
    }

    public void think() {
        words = "new thought";
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

變數

變數有個descriptor屬性,即描述符,是個字串,每種型別都對應一個字串:

java型別descriptor
boolean Z
char C
byte B
short S
int I
float F
long J
double D
Object Ljava/lang/Object;
int[] [I
Object[][] [[Ljava/lang/Object;

刪除成員變數

刪除Hello.class中的變數value

public class ASMTest extends ClassAdapter {

    public ASMTest(ClassVisitor cv) {
        super(cv);
    }

    public FieldVisitor visitField(final int access, final String name,final String desc, final String signature, final Object value) {
        if (name.equals("value")) {
            return null;
        }
        return cv.visitField(access, name, desc, signature, value);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

Hello.class的所有成員變數會依次傳入visitField()。當傳入的變數名為value時,直接return null;,會將該變數從class樹中刪除。

修改成員變數許可權

修改Hello.class中的變數value許可權為public

public class ASMTest extends ClassAdapter {

    public ASMTest(ClassVisitor cv) {
        super(cv);
    }

    public FieldVisitor visitField(final int access, final String name,final String desc, final String signature, final Object value) {
        if (name.equals("value")) {
            return cv.visitField(Opcodes.ACC_PUBLIC, name, desc, signature, value);
        }
        return cv.visitField(access, name, desc, signature, value);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

新增成員變數

為Hello.class新增int變數tInt,其初始值為3。

public class ASMTest extends ClassAdapter {

    public ASMTest(ClassVisitor cv) {
        super(cv);
    }

    public void visitEnd() {
        FieldVisitor fv = this.cv.visitField(Opcodes.ACC_PRIVATE, "temp", "I", null, 3);
        if (fv!=null){
            fv.visitEnd();
        }
        super.visitEnd();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

要點:

  1. 在所有欄位都訪問完成後,會呼叫ClassVisitor.visitEnd()作為結束,此時即可進行成員變數的新增。
  2. 呼叫visitField()來訪問成員變數。若要訪問的成員變數不存在,則建立。

visitField()的定義為:

public FieldVisitor visitField(int access,
                               java.lang.String name,
                               java.lang.String descriptor,
                               java.lang.String signature,
                               java.lang.Object value)
  • 1
  • 2
  • 3
  • 4
  • 5

其中:

  • descriptor: 型別描述符,即該成員變數的型別。
  • signature: 簽名。預設null即可。
  • value: 初始值。

方法

方法的描述符descriptor是個字串,包含:

java型別descriptor
void m(int i, float) (IF)V
int m(Object o) (Ljava/lang/Object;)I
int[] m(int i, String s) (ILjava/lang/String;)[I
Object m(int[] i) ([I)Ljava/lang/Object;

包含兩部分:

  • ()內的是引數型別,多個引數直接拼接即可。例如(IF),指有2個引數,第一個是int,第二個是float
  • ()後的是返回值型別。

刪除方法

刪除Hello.class中的方法think

public class ASMTest extends ClassAdapter {

    public ASMTest(ClassVisitor cv) {
        super(cv);
    }

    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        if (name.equals("think")) {
            return null;
        }
        return this.cv.visitMethod(access, name, desc, signature, exceptions);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

Hello.class的所有方法會依次傳入visitMethod()。當傳入的方法名為think時,直接return null;,會將該方法從class樹中刪除。
上面這種寫法是使用方法名來進行判斷。然而,有些類有多個同名方法,使用上面的寫法會將所有同名方法都刪除。若要只刪除某個方法,則需要同時對descriptor進行判斷:

if (name.equals("think") && desc.equals("I")) {
      return null;
}
  • 1
  • 2
  • 3

修改/新增

方法的修改/新增較為複雜。對於修改,常規做法是攔截到目標方法後,返回一個新的MethodVisitor

public class ASMTest extends ClassAdapter {

    public ASMTest(ClassVisitor cv) {
        super(cv);
    }

    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        if (name.equals("think")) {
			MethodVisitor mv = cv.visitMethod(access, name, desc, signature,exceptions);
            return new NewMethodAdapter(mv);
        }
        return this.cv.visitMethod(access, name, desc, signature, exceptions);
    }
}
class NewMethodAdapter extends MethodAdapter {
    public NewMethodAdapter(MethodVisitor mv) {
        super(mv);
    }

    public void visitCode() {}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

MethodAdapter繼承自MethodVisitor。通過過載MethodAdapter的各個方法來實現對類方法的修改。可參考MethodVisitor官方文件