Najrazumljiviji ASM tutorijal ikada
Jedna misao
We are all in the gutter, but some of us are looking at the stars. (Svi mi živimo u jarku, ali neki od nas gledaju u zvezde) — Oscar Wilde, „Lepeza ledi Vindermir"
Svet je zamućen, a ja jedini čist; svi su pijani, a ja jedini trijezan — Qu Yuan, „Pesme Čua"
Predgovor
ASM je opšte namenski Java okvir za obradu i analizu bytecode-a. Može se koristiti za izmenu postojećih class fajlova ili za dinamičko generisanje class fajlova.
ASM is an all purpose Java bytecode manipulation and analysis framework. It can be used to modify existing classes or to dynamically generate classes, directly in binary form. ASM provides some common bytecode transformations and analysis algorithms from which custom complex transformations and code analysis tools can be built. ASM offers similar functionality as other Java bytecode frameworks, but is focused onperformance. Because it was designed and implemented to be as small and as fast as possible, it is well suited for use in dynamic systems (but can of course be used in a static way too, e.g. in compilers).
Ovaj članak deli razumevanje i primenu ASM-a; za to je prethodno potrebno da ovladamo bytecode-om class fajla, modelom dizajna JVM-a zasnovanim na steku i instrukcijama JVM-a.
Bytecode class fajla
Java fajl koji napišemo prevodi se komandom javac u class fajl, koji JVM na kraju izvršava kako bi pokrenuo program. Struktura class fajla izgleda ovako:
ClassFile {
u4 magic; // 0xCAFEBABE
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[];
u2 fields_count;
field_info fields[];
u2 methods_count;
method_info methods[];
u2 attributes_count;
attribute_info attributes[];
}U nastavku ćemo to objasniti kroz jednostavan primer. U nastavku je jednostavan Java fajl koji smo napisali, sa samo prostim pozivima funkcija.
public class Test {
private int num1 = 1;
public static int NUM1 = 100;
public int func(int a,int b){
return add(a,b);
}
public int add(int a,int b) {
return a+b+num1;
}
public int sub(int a, int b) {
return a-b-NUM1;
}
}Komandom javac -g Test.java prevedimo kod u class fajl, a zatim komandom javap -verbose Test.class pregledajmo format class fajla.
public class com.wuba.asmdemo.Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#26 // java/lang/Object."<init>":()V
#2 = Fieldref #5.#27 // com/wuba/asmdemo/Test.num1:I
#3 = Methodref #5.#28 // com/wuba/asmdemo/Test.add:(II)I
#4 = Fieldref #5.#29 // com/wuba/asmdemo/Test.NUM1:I
#5 = Class #30 // com/wuba/asmdemo/Test
#6 = Class #31 // java/lang/Object
#7 = Utf8 num1
#8 = Utf8 I
#9 = Utf8 NUM1
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 LocalVariableTable
#15 = Utf8 this
#16 = Utf8 Lcom/wuba/asmdemo/Test;
#17 = Utf8 func
#18 = Utf8 (II)I
#19 = Utf8 a
#20 = Utf8 b
#21 = Utf8 add
#22 = Utf8 sub
#23 = Utf8 <clinit>
#24 = Utf8 SourceFile
#25 = Utf8 Test.java
#26 = NameAndType #10:#11 // "<init>":()V
#27 = NameAndType #7:#8 // num1:I
#28 = NameAndType #21:#18 // add:(II)I
#29 = NameAndType #9:#8 // NUM1:I
#30 = Utf8 com/wuba/asmdemo/Test
#31 = Utf8 java/lang/Object
{
public static int NUM1;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
public com.wuba.asmdemo.Test(); // konstruktor
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field num1:I
9: return
LineNumberTable:
line 3: 0
line 5: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/wuba/asmdemo/Test;
public int func(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=3
0: aload_0
1: iload_1
2: iload_2
3: invokevirtual #3 // Method add:(II)I
6: ireturn
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lcom/wuba/asmdemo/Test;
0 7 1 a I
0 7 2 b I
public int add(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: iload_1
1: iload_2
2: iadd
3: aload_0
4: getfield #2 // Field num1:I
7: iadd
8: ireturn
LineNumberTable:
line 14: 0
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/wuba/asmdemo/Test;
0 9 1 a I
0 9 2 b I
public int sub(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: iload_1
1: iload_2
2: isub
3: getstatic #4 // Field NUM1:I
6: isub
7: ireturn
LineNumberTable:
line 18: 0
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 this Lcom/wuba/asmdemo/Test;
0 8 1 a I
0 8 2 b I
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 100
2: putstatic #4 // Field NUM1:I
5: return
LineNumberTable:
line 7: 0
}
SourceFile: "Test.java"Vidi se da posle prevođenja u class fajl imena polja, imena metoda, imena tipova itd. postoje u constant pool-u, čime se postiže cilj smanjenja fajla. Ujedno, definicija metoda pretvorena je u JVM instrukcije. U nastavku je potrebno da produbimo razumevanje JVM instrukcija; pre toga moramo razumeti model dizajna JVM-a zasnovan na steku.
Model dizajna JVM-a zasnovan na steku
Skup instrukcija JVM-a zasnovan je na steku, a ne na registrima; model zasnovan na steku ima vrlo dobru platformsku prenosivost. Kada se u niti izvršava metod, mi ćemo kreirati okvir steka, potisnuti ga na stek i izvršiti; ako taj metod poziva drugi metod, ponovo se kreira novi okvir steka koji se potiskuje na stek. Pri povratku metoda, izvorni okvir steka vraća rezultat izvršenja metoda prethodnom okviru steka, a zatim virtuelna mašina odbacuje taj okvir steka.
Tabela lokalnih promenljivih
Tabela lokalnih promenljivih (Local Variable Table) jeste skup prostora za čuvanje vrednosti promenljivih; služi za smeštanje argumenata metoda i lokalnih promenljivih definisanih u metodi. Virtuelna mašina pronalazi odgovarajuću lokalnu promenljivu metodom lociranja po indeksu. Recimo, za primer gore navedenog koda:
public int sub(int a, int b) {
return a-b-NUM1;
}Možete pogoditi koje su lokalne promenljive ovog metoda? Odgovor je 3 — zar ne trebalo bi da budu samo a i b? Postoji još i this; za metode instanci kompilator uvek dodaje argument this. Ukoliko je u pitanju statički metod, onda ih je 2.
public int sub(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: iload_1
1: iload_2
2: isub
3: getstatic #4 // Field NUM1:I
6: isub
7: ireturn
LineNumberTable:
line 18: 0
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 this Lcom/wuba/asmdemo/Test;
0 8 1 a I
0 8 2 b IDakle, u tabeli lokalnih promenljivih element sa indeksom 0 jeste this, prvi je a, a drugi b.
Operand stek
Pomoću tabele lokalnih promenljivih imamo podatke koje treba obraditi i ažurirati, ali kako obraditi te podatke lokalnih promenljivih? Preko operand steka. Kada metod tek počne da se izvršava, njegov operand stek je prazan; sa tokom izvršenja metoda i izvršavanjem instrukcija bytecode-a, konstante ili promenljive se iz tabele lokalnih promenljivih ili polja instance objekta kopiraju i upisuju na operand stek, a zatim se, sa tokom računanja, elementi sa steka skidaju natrag u tabelu lokalnih promenljivih ili vraćaju pozivaocu metoda — dakle u pitanju su operacije skidanja (pop)/potiskivanja (push). Za vreme izvršenja jednog metoda često se javlja više takvih procesa skidanja/potiskivanja.
JVM instrukcije
- Komanda load: služi za učitavanje promenljive odgovarajućeg tipa sa zadate pozicije u tabeli lokalnih promenljivih na vrh operand steka;
- Komanda store: služi za smeštanje podatka odgovarajućeg tipa sa vrha operand steka na zadatu poziciju u tabeli lokalnih promenljivih;
- invokevirtual: poziva metodu instance;
- ireturn: tekuća metoda vraća int.
Još jedan primer
Promene operand steka i tabele lokalnih promenljivih tokom izvršenja bytecode-a izraza a = b + c prikazane su u nastavku:
iload_1 (b): stek = [ b ]
iload_2 (c): stek = [ b, c ]
iadd: skida b i c, gura zbir → stek = [ b+c ]
istore_1 (a): skida vrednost sa vrha → tabela lokalnih promenljivih: a = b+cASM operacije
Kroz gore navedeni uvod detaljnije smo upoznali bytecode i JVM instrukcije; sada pogledajmo kako ASM uređuje bytecode class fajla.
ASM API
ASM API je zasnovan na obrascu posetilaca i pruža nam ClassVisitor, MethodVisitor i FieldVisitor API interfejse. Kad god ASM uoči polje klase, pozvaće metod visitField; kada uoči metod klase, pozvaće MethodVisitor. Pogledajmo API interfejse.
Analiza metoda ClassVisitor
public abstract class ClassVisitor {
......
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces);
// povratni poziv pri pristupanju polju klase
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value);
// povratni poziv pri pristupanju metodi klase
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions);
public void visitEnd();
}Analiza metoda MethodVisitor
public abstract class MethodVisitor {
......
public void visitParameter(String name, int access);
// posećuje instrukciju lokalnog tipa varijable; opcode može biti jedan od LOAD, STORE, RET;
public void visitIntInsn(int opcode, int operand);
// instrukcija za rad sa poljima, služi za učitavanje ili skladištenje Field-a objekta
public void visitFieldInsn(int opcode, String owner, String name, String descriptor);
// posećuje instrukciju operacije metode
public void visitMethodInsn(int opcode, String owner, String name, String descriptor);
public void visitEnd();
}Demo ASM upotrebe
Java izvorni kod
public int add(int a,int b) {
return a+b+num1;
}Bytecode class fajla
public int add(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: iload_1
1: iload_2
2: iadd
3: aload_0
4: getfield #2 // Field num1:I
7: iadd
8: ireturn
LineNumberTable:
line 14: 0
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/wuba/asmdemo/Test;
0 9 1 a I
0 9 2 b IOdgovarajući ASM API
mv = cw.visitMethod(ACC_PUBLIC, "add", "(II)I", null, null);
mv.visitCode();
mv.visitVarInsn(ILOAD, 1);
mv.visitVarInsn(ILOAD, 2);
mv.visitInsn(IADD);
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "com/wuba/asmdemo/Test", "num1", "I");
mv.visitInsn(IADD);
mv.visitInsn(IRETURN);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLocalVariable("this", "Lcom/wuba/asmdemo/Test;", null, l0, l1, 0);
mv.visitLocalVariable("a", "I", null, l0, l1, 1);
mv.visitLocalVariable("b", "I", null, l0, l1, 2);
mv.visitMaxs(2, 3);
mv.visitEnd();Vidi se da ASM operiše bytecode-om na nivou instrukcija i da je time znatno bliži samom bytecode-u class fajla. Ukoliko imate neke potrebe za obradom bytecode-a, ASM zasigurno to može da ostvari; samo je njegovo korišćenje pomalo komplikovanije. Ovde toplo preporučujem jedan ASM dodatak
https://plugins.jetbrains.com/plugin/5918-asm-bytecode-outline
koji može jednim klikom generisati odgovarajući ASM API kod — u IDEA-i se instalira iz prodavnice dodataka („ASM Bytecode Outline”), a zatim se desnim klikom na fajl klase bira Show Bytecode Outline: pored bytecode-a prikazuje se gotova ASM implementacija (niz poziva visitMethod, visitFieldInsn itd.) koji možete direktno iskopirati u svoj ClassWriter kod.
Referentni link: (Vidi odgovarajuću raspravu na temu)
