Obsah
1. Pohled pod kapotu JVM – tvorba programových smyček s využitím nástroje Javassist
2. Ukázky různých způsobů generování bajtkódu počítané programové smyčky for
3. Generování metody s počítanou programovou smyčkou for v Javassistu
4. Výpočet cílů podmíněných a nepodmíněných skoků
5. Tvorba bajtkódu programové smyčky – složitější varianta
6. Tvorba bajtkódu programové smyčky – jednodušší varianta
7. Vylepšený výpis struktur bajtkódů obou testovacích metod loopTest1() a loopTest2()
8. Úplný zdrojový kód demonstračního příkladu ClassGenerationTest9
9. Výstup demonstračního příkladu ClassGenerationTest9
10. Bajtkód třídy GeneratedClass9
11. Repositář se zdrojovými kódy dnešního demonstračního příkladu ClassGenerationTest9
1. Pohled pod kapotu JVM – tvorba programových smyček s využitím nástroje Javassist
V dnešní části seriálu o programovacím jazyku Java i o virtuálním stroji Javy si ukážeme, jak je možné nástroj Javassist využít pro generování bajtkódu metod, v jejichž těle se nachází počítané programové smyčky či běžné smyčky s podmínkou testovanou na začátku či na konci každé iterace. Navážeme tak na předchozí část, v níž jsme „pouze“ upravili již existující bajtkód metody s programovou smyčkou. V současné verzi Javassistu sice není tvorba programových smyček zcela jednoduchá, ovšem uvidíme, že i přes některá omezení, spočívající v nutnosti použít nízkoúrovňové operace, je Javassist možné k této činnosti využít. Jen pro úplnost si připomeňme, jaké instrukce se v bajtkódu virtuálního stroje Javy používají v případě potřeby provést nepodmíněný či podmíněný skok. Tabulka zobrazená pod tímto odstavcem vznikla sloučením tabulek popsaných v předchozí části tohoto seriálu:
# | Instrukce | Opkód | Operandy | Popis | |
---|---|---|---|---|---|
1 | goto | 0×A7 | highbyte, lowbyte | přímý skok na adresu uloženou v dvojici operandů: highbyte*256+lowbyte | |
2 | goto_w | 0×C8 | byte1,byte2,byte3 byte4 | přímý skok na adresu uloženou ve čtveřici operandů: byte1*224+byte2*216+byte3*28+byte4 | |
3 | ifeq | 0×99 | highbyte, lowbyte | TOS=0 | skok na lokální adresu highbyte*256+lowbyte při splnění podmínky |
4 | ifne | 0×9A | highbyte, lowbyte | TOS≠0 | skok na lokální adresu highbyte*256+lowbyte při splnění podmínky |
5 | iflt | 0×9B | highbyte, lowbyte | TOS<0 | skok na lokální adresu highbyte*256+lowbyte při splnění podmínky |
6 | ifge | 0×9C | highbyte, lowbyte | TOS≥0 | skok na lokální adresu highbyte*256+lowbyte při splnění podmínky |
7 | ifgt | 0×9D | highbyte, lowbyte | TOS>0 | skok na lokální adresu highbyte*256+lowbyte při splnění podmínky |
8 | ifle | 0×9E | highbyte, lowbyte | TOS≤0 | skok na lokální adresu highbyte*256+lowbyte při splnění podmínky |
9 | if_icmpeq | 0×9F | highbyte, lowbyte | value1=value2 | skok na adresu highbyte*256+lowbyte při splnění podmínky |
10 | if_icmpne | 0×A0 | highbyte, lowbyte | value1≠value2 | skok na adresu highbyte*256+lowbyte při splnění podmínky |
11 | if_icmplt | 0×A1 | highbyte, lowbyte | value1<value2 | skok na adresu highbyte*256+lowbyte při splnění podmínky |
12 | if_icmpge | 0×A2 | highbyte, lowbyte | value1≥value2 | skok na adresu highbyte*256+lowbyte při splnění podmínky |
13 | if_icmpgt | 0×A3 | highbyte, lowbyte | value1>value2 | skok na adresu highbyte*256+lowbyte při splnění podmínky |
14 | if_icmple | 0×A4 | highbyte, lowbyte | value1≤value2 | skok na adresu highbyte*256+lowbyte při splnění podmínky |
Jak si řekneme v navazujícím textu, jsou adresy cílů skoků uvedeny relativně vůči aktuálnímu indexu instrukce, což je velmi důležité při výpočtu cíle skoku.
2. Ukázky různých způsobů generování bajtkódu počítané programové smyčky for
Nyní se dostáváme k poměrně důležité problematice týkající se způsobu překladu zdrojových kódů Javy do bajtkódu. Obecně je možné říci, že se překladače Javy nesnaží o provádění žádných zásadních optimalizací, protože výsledný bajtkód není ve většině případů interpretován, ale následně přeložen do nativního zdrojového kódu pomocí JIT překladače. I z tohoto důvodu je většinou generovaný bajtkód poměrně snadno čitelný a dobře odráží původní algoritmus napsaný přímo v Javě. Ovšem určitý problém spočívá v tom, že i přes přesnou definici syntaxe a sémantiky programovacího jazyka Java (Java Language Specification – JLS) i přesnou specifikaci bajtkódu a významu jednotlivých instrukcí není v žádné normě popsáno, jakým způsobem se mají jednotlivé bloky zdrojového kódu zkompilovat do sekvence instrukcí JVM. To jinými slovy znamená, že každý překladač Javy může generovat odlišné sekvence instrukcí a přitom bude stále splňovat jak JLS, tak i JVM Specification (na tuto vlastnost se nelze dívat jako na nedostatek, ale spíše na svobodu volby tvůrců překladačů).
Ukažme si však konkrétní příklad, například jednoduchou počítanou smyčku for:
/** * Testovaci trida. */ public class LoopTest { private static void loopTest1() { int i = 0; while (i < 10) { System.out.println("Hello world!"); i++; } } }
Překladač javac vytvoří následující sekvenci instrukcí, v níž se test počitadla smyčky na koncovou hodnotu provádí v instrukcích 2, 3 a 5, zatímco na konci smyčky můžeme nalézt nepodmíněný skok goto:
private static void loopTest1(); Code: 0: iconst_0 1: istore_0 2: iload_0 3: bipush 10 5: if_icmpge 22 8: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream; 11: ldc #3; //String Hello world! 13: invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 16: iinc 0, 1 19: goto 2 22: return
Naproti tomu ecj (záleží ovšem na verzi) vytvoří poněkud odlišný kód, v němž je test na hodnotu počitadla uveden na konci smyčky, což ale znamená, že ihned na počátku (před první iterací) je nutné se pomocí nepodmíněného skoku na tento test přesunout:
private static void loopTest1(); Code: 0: iconst_0 1: istore_0 2: goto 16 5: getstatic #20; //Field java/lang/System.out:Ljava/io/PrintStream; 8: ldc #22; //String Hello world! 10: invokevirtual #28; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 13: iinc 0, 1 16: iload_0 17: bipush 10 19: if_icmplt 5 22: return
Povšimněte si, že v obou případech má bajtkód délku 23 bajtů a obsahuje jeden nepodmíněný skok goto a jeden skok podmíněný, a to buď if_icmpge nebo k němu inverzní test if_icmplt.
3. Generování metody s počítanou programovou smyčkou for v Javassistu
V této kapitole i v kapitolách následujících jsou popsány některé důležité metody, které jsou součástí demonstračního příkladu nazvaného ClassGenerationTest9. Tento příklad po svém spuštění vytvoří bajtkód nové třídy GeneratedClass9 se statickými metodami main(), loopTest1() a loopTest2(). Obě zmíněné metody loopTest*() po svém zavolání vytisknou deset řádků obsahujících shodný textový řetězec. Bajtkódy těchto metod jsou ve skutečnosti prakticky shodné, ovšem pro jejich vytvoření použijeme nejdříve složitější postup s nízkoúrovňovými operacemi a u druhé metody pak postup, v němž se bude používat vyšší úroveň abstrakce. Bajtkódy metod jsou stále generovány stejným způsobem – nejprve se vytvoří vlastní záznam o metodě, včetně údajů o jejích příznacích, návratovém typu i typu jednotlivých parametrů a posléze se každé metodě přiřadí atribut „CODE“ s vlastním bajtkódem.
Vytvoření metody loopTest1():
/** * Vytvoreni staticke metody loopTest1() bez navratove hodnoty. * Instrukce tvorici telo metody jsou vytvoreny s vyuzitim tridy Bytecode. * * @param generatedClass * predstavuje vytvarenou tridu * @throws CannotCompileException * vyhozena v pripade chyby ve zdrojovem kodu */ private static void constructMethodLoopTest1(CtClass generatedClass) throws CannotCompileException { MethodInfo methodInfo = prepareMethod(generatedClass, "loopTest1"); // vytvoreni tela metody ConstPool constPool = methodInfo.getConstPool(); Bytecode bytecode = generateBytecodeForMethodLoopTest1(constPool); addCodeAttributeToGeneratedMethod(methodInfo, bytecode); }
Vytvoření metody loopTest2() je prakticky shodné:
/** * Vytvoreni staticke metody loopTest2() bez navratove hodnoty. * Instrukce tvorici telo metody jsou vytvoreny s vyuzitim tridy Bytecode. * * @param generatedClass * predstavuje vytvarenou tridu * @throws CannotCompileException * vyhozena v pripade chyby ve zdrojovem kodu */ private static void constructMethodLoopTest2(CtClass generatedClass) throws CannotCompileException { MethodInfo methodInfo = prepareMethod(generatedClass, "loopTest2"); // vytvoreni tela metody ConstPool constPool = methodInfo.getConstPool(); Bytecode bytecode = generateBytecodeForMethodLoopTest2(constPool); addCodeAttributeToGeneratedMethod(methodInfo, bytecode); }
Uživatelská metoda prepareMethod() je taktéž shodná pro vytvoření loopTest1() i loopTest2(), protože se v ní nastavují shodné modifikátory, stejný návratový typ (void) i stejné typy parametrů (nulový počet):
/** * Priprava bajtkodu metody. * * @param generatedClass * predstavuje vytvarenou tridu * @param methodName * jmeno vytvarene metody * @throws CannotCompileException * vyhozena v pripade chyby ve zdrojovem kodu */ private static MethodInfo prepareMethod(CtClass generatedClass, String methodName) throws CannotCompileException { CtClass returnType = CtClass.voidType; CtClass[] parameterTypes = {}; // u metody je nutne znat jeji jmeno, navratovou hodnotu i typy parametru CtMethod loopTestMethod = new CtMethod(returnType, methodName, parameterTypes, generatedClass); // zmena modifikatoru loopTestMethod.setModifiers(Modifier.STATIC | Modifier.PUBLIC); // telo metody generatedClass.addMethod(loopTestMethod); MethodInfo methodInfo = loopTestMethod.getMethodInfo(); return methodInfo; }
Přiřazení atributu „CODE“ k vygenerované metodě je velmi jednoduchý úkon:
/** * Pridani atributu "CODE" k vygenerovane metode. * * @param methodInfo * @param bytecode */ private static void addCodeAttributeToGeneratedMethod(MethodInfo methodInfo, Bytecode bytecode) { CodeAttribute codeAttribute = bytecode.toCodeAttribute(); methodInfo.setCodeAttribute(codeAttribute); }
4. Výpočet cílů podmíněných a nepodmíněných skoků
Jak jsme si již ukázali ve druhé kapitole, nevyhneme se při implementaci počítaných programových smyček použití nepodmíněných i podmíněných skoků, u nichž je nutné vhodným způsobem vypočítat jejich cíl, tj. adresu, na kterou se má skok provést. Připomeňme si, že instrukce jsou adresovány v každé metodě od nuly, protože ve virtuálním stroji Javy neexistuje přímo adresovatelný globální adresový prostor. Navíc jsou cíle skoků zapsány ve formě offsetů, tj. relativních adres, které je nutné nejprve připočíst k adrese zpracovávané instrukce, abychom získali adresu absolutní (přesněji řečeno absolutní v rámci lokálního adresního prostoru metody). Jedním z problémů, které musíme vyřešit, je tedy výpočet již zmíněného offsetu. K tomu nám dopomůže metoda Bytecode.currentPc() vracející index právě zapsané instrukce. Tento index lze uložit do pomocné proměnné a následně ho na vhodném místě využít pro výpočet offsetu.
Dále musíme využít dvě nové metody: Bytecode.addGap(počet) pro vložení neinicializovaných bajtů do bajtkódu a metodu Bytecode.write(index, data) pro zápis hodnoty bajtu do bajtkódu na adresu určenou indexem. Díky existenci této metody je možné se vrátit k oblasti vyplněné pomocí Bytecode.addGap() a uložit na toto místo vypočtenou adresu cíle skoku.
Podívejme se nyní na způsob vložení instrukce GOTO provádějící dopředný skok. Při dopředném skoku ještě nemusíme přesně vědět hodnotu offsetu, to však není nutné. Nejprve vložíme operační kód instrukce GOTO, za níž ponecháme dva volné bajty. Současně si zapamatujeme index instrukce GOTO (to pro výpočet offsetu) i index prvního volného bajtu:
// za instrukci GOTO nasleduje 16bitovy cil skoku // - tyto dva bajty prozatim preskocime // - a zapamatujeme si jejich index pro pozdejsi inicializaci int gotoInstructionIndex = bytecode.currentPc(); bytecode.addOpcode(Opcode.GOTO); int gotoOperandIndex = bytecode.currentPc(); bytecode.addGap(2);
Na místě, kam má skok směřovat, opět získáme hodnotu indexu:
// ted jiz vime, ze sem bude smerovat cil skoku int gotoDestinationPC = bytecode.currentPc();
Na závěr generování bajtkódu již snadno vypočteme offset a zapíšeme jeho dva bajty do výplně za instrukcí GOTO. Připomeňme si, že offset může být jak kladný (dopředný skok), tak i záporný (skok zpět):
// nyni je jiz mozne vyplnit cilovou adresu skoku int offset = gotoDestinationPC - gotoInstructionIndex; bytecode.write(gotoOperandIndex, offset >> 8); bytecode.write(gotoOperandIndex+1, offset);
5. Tvorba bajtkódu programové smyčky – složitější varianta
V této chvíli již máme všechny potřebné znalosti nutné pro vytvoření bajtkódu metod loopTest1() a loopTest2(). Metoda loopTest1() bude (zejména kvůli studijním účelům) vytvořena složitějším způsobem, v němž jsou použity především nízkoúrovňové operace, které přímo odpovídají jedenácti instrukcím bajtkódu, s jejichž pomocí má být metoda vytvořena:
0: iconst_0 1: istore_0 2: goto 16 5: getstatic #14; //Field java/lang/System.out:Ljava/io/PrintStream; 8: ldc #16; //String Hello 10: invokevirtual #22; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 13: iinc 0, 1 16: iload_0 17: bipush 10 19: if_icmplt 5 22: return
Povšimněte si, jakým způsobem je uvedených jedenáct instrukcí generováno. Jedinou problematičtější část tvoří výpočet cílů skoků:
/** * Vytvoreni bajtkodu predstavujiciho sekvenci instrukci pro metodu int * loopTest1(). * * @param constPool * tabulka konstant pouzivana metodou * @return bajtkod predstavujici sekvenci instrukci pro metodu int foo() */ private static Bytecode generateBytecodeForMethodLoopTest1(ConstPool constPool) { // bajtkod by mel odpovidat nasledujicimu kodu // for (int i = 0; i < 10; i++) { // System.out.println("Hello"); // } // coz zhruba odpovida sekvenci instrukci // 0: iconst_0 // 1: istore_0 // 2: goto 16 // 5: getstatic #20; //Field java/lang/System.out:Ljava/io/PrintStream; // 8: ldc #42; //String Hello world! // 10: invokevirtual #28; //Method java/io/PrintStream.println:(Ljava/lang/String;)V // 13: iinc 0, 1 // 16: iload_0 // 17: bipush 10 // 19: if_icmplt 5 // 22: return final int stackSize = 1; final int localVars = 1; Bytecode bytecode = new Bytecode(constPool, stackSize, localVars); bytecode.addOpcode(Opcode.ICONST_0); bytecode.addOpcode(Opcode.ISTORE_0); // za instrukci GOTO nasleduje 16bitovy cil skoku // - tyto dva bajty prozatim preskocime // - a zapamatujeme si jejich index pro pozdejsi inicializaci int gotoInstructionIndex = bytecode.currentPc(); bytecode.addOpcode(Opcode.GOTO); int gotoOperandIndex = bytecode.currentPc(); bytecode.addGap(2); // v teto promenne bude ulozen index prvni instrukce smycky int loopStartIndex = bytecode.currentPc(); bytecode.addGetstatic("java.lang.System", "out", "Ljava/io/PrintStream;"); bytecode.addLdc("Hello"); bytecode.addInvokevirtual("java.io.PrintStream", "println", "(Ljava/lang/String;)V"); // zvyseni hodnoty pocitadla o jednicku bytecode.addOpcode(Opcode.IINC); bytecode.add(0, 1); // ted jiz vime, ze sem bude smerovat cil skoku int gotoDestinationPC = bytecode.currentPc(); bytecode.addOpcode(Opcode.ILOAD_0); bytecode.addOpcode(Opcode.BIPUSH); bytecode.addOpcode(10); int currentPC = bytecode.currentPc(); // na konci smycky je umisten podmineny skok bytecode.addOpcode(Opcode.IF_ICMPLT); // vypocet cile podmineneho skoku -> zacatek smycky bytecode.addIndex(loopStartIndex - currentPC); // instrukce Return bytecode.addOpcode(Opcode.RETURN); // nyni je jiz mozne vyplnit cilovou adresu skoku int offset = gotoDestinationPC - gotoInstructionIndex; bytecode.write(gotoOperandIndex, offset >> 8); bytecode.write(gotoOperandIndex+1, offset); // finito return bytecode; }
Podmíněný skok if_icmplt je prováděn směrem vzad, tudíž není nutné pro cíl skoku rezervovat žádné bajty ani do nich zpětně zapisovat pomocí Bytecode.write(). Namísto toho je možné využít praktičtější metodu Bytecode.addIndex() akceptující šestnáctibitový index.
6. Tvorba bajtkódu programové smyčky – jednodušší varianta
Jak jsme si již naznačili v předchozích kapitolách, je bajtkód metody loopTest2() sice shodný s bajtkódem metody loopTest1(), ovšem při jeho generování jsou využity poněkud abstraktnější operace. Týká se to zejména náhrady volání Bytecode.addOpcode(Opcode.ICONST0) apod. za obecnější Bytecode.addIconst(0). Nástroj Javassist sám na základě parametru rozhodne, jaká instrukce se použije, což platí i pro Bytecode.addIconst(10), což se přeloží na instrukci BIPUSH 10. Podobně je zjednodušena tvorba bajtkódu pro volání metody System.out.println() či pro návrat z metody instrukcí RETURN. Výpočet cílů skoků však není zjednodušen a stále je nutné používat pomocné proměnné s uloženými indexy instrukcí a/nebo jejich operandů:
/** * Vytvoreni bajtkodu predstavujiciho sekvenci instrukci pro metodu int * loopTest1(). * * @param constPool * tabulka konstant pouzivana metodou * @return bajtkod predstavujici sekvenci instrukci pro metodu int foo() */ private static Bytecode generateBytecodeForMethodLoopTest2(ConstPool constPool) { // bajtkod by mel odpovidat nasledujicimu kodu // for (int i = 0; i < 10; i++) { // System.out.println("world!"); // } // coz zhruba odpovida sekvenci instrukci // 0: iconst_0 // 1: istore_0 // 2: goto 16 // 5: getstatic #20; //Field java/lang/System.out:Ljava/io/PrintStream; // 8: ldc #42; //String Hello world! // 10: invokevirtual #28; //Method java/io/PrintStream.println:(Ljava/lang/String;)V // 13: iinc 0, 1 // 16: iload_0 // 17: bipush 10 // 19: if_icmplt 5 // 22: return final int stackSize = 1; final int localVars = 1; Bytecode bytecode = new Bytecode(constPool, stackSize, localVars); bytecode.addIconst(0); bytecode.addIstore(0); // za instrukci GOTO nasleduje 16bitovy cil skoku // - tyto dva bajty prozatim preskocime // - a zapamatujeme si jejich index pro pozdejsi inicializaci int gotoInstructionIndex = bytecode.currentPc(); bytecode.addOpcode(Opcode.GOTO); int gotoOperandIndex = bytecode.currentPc(); bytecode.addGap(2); // v teto promenne bude ulozen index prvni instrukce smycky int loopStartIndex = bytecode.currentPc(); bytecode.addPrintln("world!"); // zvyseni hodnoty pocitadla o jednicku bytecode.addOpcode(Opcode.IINC); bytecode.add(0, 1); // ted jiz vime, ze sem bude smerovat cil skoku int gotoDestinationPC = bytecode.currentPc(); bytecode.addIload(0); // pro hodnotu 10 se vygeneruje instrukce BIPUSH 10 bytecode.addIconst(10); // tuto hodnotu potrebujeme pro vypocet cile podmineneho skoku // (pocita se relativne vuci teto instrukci) int currentPC = bytecode.currentPc(); // na konci smycky je umisten podmineny skok bytecode.addOpcode(Opcode.IF_ICMPLT); // vypocet cile podmineneho skoku -> zacatek smycky bytecode.addIndex(loopStartIndex - currentPC); // instrukce Return bytecode.addReturn(CtClass.voidType); // nyni je jiz mozne vyplnit cilovou adresu skoku int offset = gotoDestinationPC - gotoInstructionIndex; bytecode.write(gotoOperandIndex, offset >> 8); bytecode.write(gotoOperandIndex+1, offset); // finito return bytecode; }
7. Vylepšený výpis struktur bajtkódů obou testovacích metod loopTest1() a loopTest2()
Součástí dnešního demonstračního příkladu bude i výpis bajtkódu všech vygenerovaných metod. Na rozdíl od nástroje javap však budeme pro větší názornost potřebovat, aby se kromě mnemotechnických kódů jednotlivých instrukcí zobrazily i hexadecimální hodnoty bajtů tvořících jak kód instrukce, tak i její operand či operandy. Díky tomu si budeme moci lépe ukázat, jak jsou zakódovány cíle nepodmíněných i podmíněných skoků, například:
03 iconst_0 3b istore_0 a7 00 0e goto b2 00 0e getstatic 12 10 ldc b6 00 16 invokevirtual 84 00 01 iinc 1a iload_0 10 0a bipush a1 ff f2 if_icmplt b1 return
Uživatelská metoda printMethodStructures() je ve skutečnosti velmi jednoduchá, protože pro všechny tři zkoumané metody zavolá další uživatelskou metodu printMethodStructure():
/** * Vypis struktury vybranych metod z generovane tridy. * * @param generatedClass * predstavuje vytvarenou tridu * @throws NotFoundException * vyhozena, pokud metoda nebyla nalezena * @throws BadBytecode */ private static void printMethodStructures(CtClass generatedClass) throws NotFoundException, BadBytecode { printMethodStructure(generatedClass, "main"); printMethodStructure(generatedClass, "loopTest1"); printMethodStructure(generatedClass, "loopTest2"); }
V uživatelské metodě nazvané printMethodStructure() se nejprve získá instance třídy CtMethod představující obraz zkoumané metody. Následně se přes volání CtMethod.getMethodInfo() získá instance třídy MethodInfo obsahující, jak již název této třídy napovídá, informace o zkoumané metodě. My využijeme následující čtveřici getterů, které jsou ve třídě MethodInfo deklarovány:
# | Metoda | Popis |
---|---|---|
1 | javassist.bytecode.MethodInfo.getName() | vrátí jméno metody |
2 | javassist.bytecode.MethodInfo.getDescriptor() | vrátí deskriptor metody (část její signatury) |
3 | javassist.bytecode.MethodInfo.getAccessFlags() | vrátí přístupová práva a další modifikátory metody (STATIC…) |
4 | javassist.bytecode.MethodInfo.getCodeAttribute() | vrátí atribut metody reprezentující její tělo |
V následujícím úryvku zdrojového kódu si povšimněte, jak lze velmi snadno převést modifikátory metody na řetězec s využitím Modifier.toString():
/** * Vypis struktury vybrane metody. * * @param generatedClass * predstavuje vytvarenou tridu * @param methodName * jmeno metody, jejiz struktura se ma vypsat * @throws NotFoundException * vyhozena, pokud metoda nebyla nalezena * @throws BadBytecode * vyhozena, pokud se nalezne neplatna instrukce v bytekodu */ private static void printMethodStructure(CtClass generatedClass, String methodName) throws NotFoundException, BadBytecode { System.out.println("Method '" + methodName + "' structure:"); CtMethod method = generatedClass.getDeclaredMethod(methodName); if (method == null) { System.out.println(" not found!"); return; } MethodInfo methodInfo = method.getMethodInfo(); System.out.println(" real name: " + methodInfo.getName()); System.out.println(" descriptor: " + methodInfo.getDescriptor()); System.out.println(" access flags: " + Modifier.toString(methodInfo.getAccessFlags())); System.out.println(" method body:"); printMethodBody(methodInfo); System.out.println(); }
Podívejme se nyní na další uživatelskou metodu nazvanou printMethodBody(), která vlastně představuje ústřední část našeho jednoduchého „disassembleru“. V této metodě se prochází přes jednotlivé instrukce bajtkódu (metoda CodeIterator.next() může přeskočit o více než jeden bajt) a následně jsou vypsány mnemotechnické zkratky všech přečtených instrukcí. My ovšem potřebujeme vypsat i hexadecimální hodnoty všech bajtů tvořících jednu instrukci. Kvůli tomu je vypočten index následující instrukce a následně se ve vložené programové smyčce prochází všemi indexy mezi současně zpracovávanou instrukcí a instrukcí následující. Hodnota každého bajtu uloženého na tomto indexu je vypsána a současně se i sníží hodnota pomocné proměnné spaces použité pro zarovnání názvů operačních kódů instrukcí:
/** * Vypis instrukci tvoricich telo vybrane metody. * * @throws NotFoundException * vyhozena, pokud metoda nebyla nalezena * @throws BadBytecode * vyhozena, pokud se nalezne neplatna instrukce v bytekodu */ private static void printMethodBody(MethodInfo methodInfo) throws BadBytecode { CodeAttribute ca = methodInfo.getCodeAttribute(); CodeIterator iterator = ca.iterator(); while (iterator.hasNext()) { int index = iterator.next(); int opcode = iterator.byteAt(index); int nextIndex = iterator.lookAhead(); int spaces = 16; for (int i = index; i < nextIndex; i++) { System.out.format("%02x ", iterator.byteAt(i)); spaces-=3; } for (int i = 0; i < spaces; i++) { System.out.print(' '); } System.out.println(" " + Mnemonic.OPCODE[opcode]); } }
8. Úplný zdrojový kód demonstračního příkladu ClassGenerationTest9
V této kapitole bude uveden výpis úplného zdrojového kódu dnešního demonstračního příkladu pojmenovaného ClassGenerationTest9. Po spuštění tohoto příkladu se nejdříve vytvoří bajtkód třídy GeneratedClass9, která bude obsahovat trojici statických metod main(), loopTest1() a loopTest2(), přičemž bajtkód metody loopTest1() je vytvořen na základě algoritmu uvedeného v páté kapitole a bajtkód metody loopTest2() byl vytvořen algoritmem popsaným v kapitole šesté. Po vytvoření celé třídy se následně vypíše i její struktura, a to včetně hexadecimálního výpisu bajtů tvořících operační kódy i operandy jednotlivých instrukcí bajtkódu (viz též předchozí kapitolu):
import java.io.IOException; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.Modifier; import javassist.NotFoundException; import javassist.bytecode.BadBytecode; import javassist.bytecode.Bytecode; import javassist.bytecode.CodeAttribute; import javassist.bytecode.CodeIterator; import javassist.bytecode.ConstPool; import javassist.bytecode.MethodInfo; import javassist.bytecode.Mnemonic; import javassist.bytecode.Opcode; /** * Test moznosti nastroje Javassist - vygenerovani jednoduche tridy * s metodou main a nekolika dalsimi metodami obsahujicimi programove smycky. * * @author Pavel Tisnovsky */ public class ClassGenerationTest9 { /** * Jmeno vygenerovane tridy. */ private static final String GENERATED_CLASS_NAME = "GeneratedClass9"; /** * Zdrojovy kod metody main(), ktery bude nasledne zkompilovan * do bajtkodu a zakomponovan do vytvorene tridy. */ private static final String MAIN_METHOD_SOURCE_TEXT = "public static void main(String[] args)" + "{" + " loopTest1();" + " loopTest2();" + "}"; /** * Vytvoreni metody main() z jejiho zdrojoveho kodu. * * @param generatedClass * predstavuje vytvarenou tridu * @throws CannotCompileException * vyhozena v pripade chyby ve zdrojovem kodu */ private static void addMethodMain(CtClass generatedClass) throws CannotCompileException { CtMethod methodMain = CtMethod.make(MAIN_METHOD_SOURCE_TEXT, generatedClass); generatedClass.addMethod(methodMain); } /** * Vytvoreni staticke metody loopTest1() bez navratove hodnoty. * Instrukce tvorici telo metody jsou vytvoreny s vyuzitim tridy Bytecode. * * @param generatedClass * predstavuje vytvarenou tridu * @throws CannotCompileException * vyhozena v pripade chyby ve zdrojovem kodu */ private static void constructMethodLoopTest1(CtClass generatedClass) throws CannotCompileException { MethodInfo methodInfo = prepareMethod(generatedClass, "loopTest1"); // vytvoreni tela metody ConstPool constPool = methodInfo.getConstPool(); Bytecode bytecode = generateBytecodeForMethodLoopTest1(constPool); addCodeAttributeToGeneratedMethod(methodInfo, bytecode); } /** * Vytvoreni staticke metody loopTest2() bez navratove hodnoty. * Instrukce tvorici telo metody jsou vytvoreny s vyuzitim tridy Bytecode. * * @param generatedClass * predstavuje vytvarenou tridu * @throws CannotCompileException * vyhozena v pripade chyby ve zdrojovem kodu */ private static void constructMethodLoopTest2(CtClass generatedClass) throws CannotCompileException { MethodInfo methodInfo = prepareMethod(generatedClass, "loopTest2"); // vytvoreni tela metody ConstPool constPool = methodInfo.getConstPool(); Bytecode bytecode = generateBytecodeForMethodLoopTest2(constPool); addCodeAttributeToGeneratedMethod(methodInfo, bytecode); } /** * Pridani atributu "CODE" k vygenerovane metode. * * @param methodInfo * @param bytecode */ private static void addCodeAttributeToGeneratedMethod(MethodInfo methodInfo, Bytecode bytecode) { CodeAttribute codeAttribute = bytecode.toCodeAttribute(); methodInfo.setCodeAttribute(codeAttribute); } /** * Priprava bajtkodu metody. * * @param generatedClass * predstavuje vytvarenou tridu * @param methodName * jmeno vytvarene metody * @throws CannotCompileException * vyhozena v pripade chyby ve zdrojovem kodu */ private static MethodInfo prepareMethod(CtClass generatedClass, String methodName) throws CannotCompileException { CtClass returnType = CtClass.voidType; CtClass[] parameterTypes = {}; // u metody je nutne znat jeji jmeno, navratovou hodnotu i typy parametru CtMethod loopTestMethod = new CtMethod(returnType, methodName, parameterTypes, generatedClass); // zmena modifikatoru loopTestMethod.setModifiers(Modifier.STATIC | Modifier.PUBLIC); // telo metody generatedClass.addMethod(loopTestMethod); MethodInfo methodInfo = loopTestMethod.getMethodInfo(); return methodInfo; } /** * Vytvoreni bajtkodu predstavujiciho sekvenci instrukci pro metodu int * loopTest1(). * * @param constPool * tabulka konstant pouzivana metodou * @return bajtkod predstavujici sekvenci instrukci pro metodu int foo() */ private static Bytecode generateBytecodeForMethodLoopTest1(ConstPool constPool) { // bajtkod by mel odpovidat nasledujicimu kodu // for (int i = 0; i < 10; i++) { // System.out.println("Hello"); // } // coz zhruba odpovida sekvenci instrukci // 0: iconst_0 // 1: istore_0 // 2: goto 16 // 5: getstatic #20; //Field java/lang/System.out:Ljava/io/PrintStream; // 8: ldc #42; //String Hello world! // 10: invokevirtual #28; //Method java/io/PrintStream.println:(Ljava/lang/String;)V // 13: iinc 0, 1 // 16: iload_0 // 17: bipush 10 // 19: if_icmplt 5 // 22: return final int stackSize = 1; final int localVars = 1; Bytecode bytecode = new Bytecode(constPool, stackSize, localVars); bytecode.addOpcode(Opcode.ICONST_0); bytecode.addOpcode(Opcode.ISTORE_0); // za instrukci GOTO nasleduje 16bitovy cil skoku // - tyto dva bajty prozatim preskocime // - a zapamatujeme si jejich index pro pozdejsi inicializaci int gotoInstructionIndex = bytecode.currentPc(); bytecode.addOpcode(Opcode.GOTO); int gotoOperandIndex = bytecode.currentPc(); bytecode.addGap(2); // v teto promenne bude ulozen index prvni instrukce smycky int loopStartIndex = bytecode.currentPc(); bytecode.addGetstatic("java.lang.System", "out", "Ljava/io/PrintStream;"); bytecode.addLdc("Hello"); bytecode.addInvokevirtual("java.io.PrintStream", "println", "(Ljava/lang/String;)V"); // zvyseni hodnoty pocitadla o jednicku bytecode.addOpcode(Opcode.IINC); bytecode.add(0, 1); // ted jiz vime, ze sem bude smerovat cil skoku int gotoDestinationPC = bytecode.currentPc(); bytecode.addOpcode(Opcode.ILOAD_0); bytecode.addOpcode(Opcode.BIPUSH); bytecode.addOpcode(10); int currentPC = bytecode.currentPc(); // na konci smycky je umisten podmineny skok bytecode.addOpcode(Opcode.IF_ICMPLT); // vypocet cile podmineneho skoku -> zacatek smycky bytecode.addIndex(loopStartIndex - currentPC); // instrukce Return bytecode.addOpcode(Opcode.RETURN); // nyni je jiz mozne vyplnit cilovou adresu skoku int offset = gotoDestinationPC - gotoInstructionIndex; bytecode.write(gotoOperandIndex, offset >> 8); bytecode.write(gotoOperandIndex+1, offset); // finito return bytecode; } /** * Vytvoreni bajtkodu predstavujiciho sekvenci instrukci pro metodu int * loopTest1(). * * @param constPool * tabulka konstant pouzivana metodou * @return bajtkod predstavujici sekvenci instrukci pro metodu int foo() */ private static Bytecode generateBytecodeForMethodLoopTest2(ConstPool constPool) { // bajtkod by mel odpovidat nasledujicimu kodu // for (int i = 0; i < 10; i++) { // System.out.println("world!"); // } // coz zhruba odpovida sekvenci instrukci // 0: iconst_0 // 1: istore_0 // 2: goto 16 // 5: getstatic #20; //Field java/lang/System.out:Ljava/io/PrintStream; // 8: ldc #42; //String Hello world! // 10: invokevirtual #28; //Method java/io/PrintStream.println:(Ljava/lang/String;)V // 13: iinc 0, 1 // 16: iload_0 // 17: bipush 10 // 19: if_icmplt 5 // 22: return final int stackSize = 1; final int localVars = 1; Bytecode bytecode = new Bytecode(constPool, stackSize, localVars); bytecode.addIconst(0); bytecode.addIstore(0); // za instrukci GOTO nasleduje 16bitovy cil skoku // - tyto dva bajty prozatim preskocime // - a zapamatujeme si jejich index pro pozdejsi inicializaci int gotoInstructionIndex = bytecode.currentPc(); bytecode.addOpcode(Opcode.GOTO); int gotoOperandIndex = bytecode.currentPc(); bytecode.addGap(2); // v teto promenne bude ulozen index prvni instrukce smycky int loopStartIndex = bytecode.currentPc(); bytecode.addPrintln("world!"); // zvyseni hodnoty pocitadla o jednicku bytecode.addOpcode(Opcode.IINC); bytecode.add(0, 1); // ted jiz vime, ze sem bude smerovat cil skoku int gotoDestinationPC = bytecode.currentPc(); bytecode.addIload(0); // pro hodnotu 10 se vygeneruje instrukce BIPUSH 10 bytecode.addIconst(10); // tuto hodnotu potrebujeme pro vypocet cile podmineneho skoku // (pocita se relativne vuci teto instrukci) int currentPC = bytecode.currentPc(); // na konci smycky je umisten podmineny skok bytecode.addOpcode(Opcode.IF_ICMPLT); // vypocet cile podmineneho skoku -> zacatek smycky bytecode.addIndex(loopStartIndex - currentPC); // instrukce Return bytecode.addReturn(CtClass.voidType); // nyni je jiz mozne vyplnit cilovou adresu skoku int offset = gotoDestinationPC - gotoInstructionIndex; bytecode.write(gotoOperandIndex, offset >> 8); bytecode.write(gotoOperandIndex+1, offset); // finito return bytecode; } /** * Vytvoreni tridy s metodou main(). * * @throws CannotCompileException * vyhozena v pripade chyby ve zdrojovem kodu metody main() * @throws IOException * pokud dojde k chybe pri zapisu bajtkodu na disk * @throws NotFoundException * pokud dojde k chybe pri zapisu bajtkodu na disk */ private static CtClass generateClass() throws CannotCompileException, NotFoundException, IOException { // ziskat vychozi class pool ClassPool pool = ClassPool.getDefault(); // vytvoreni nove verejne tridy CtClass generatedClass = pool.makeClass(GENERATED_CLASS_NAME); // konstrukce nove metody loopTest1() constructMethodLoopTest1(generatedClass); // konstrukce nove metody loopTest2() constructMethodLoopTest2(generatedClass); // pridani metody do teto tridy addMethodMain(generatedClass); // ulozeni bajtkodu na disk generatedClass.writeFile(); return generatedClass; } /** * Vypis struktury vybrane metody. * * @param generatedClass * predstavuje vytvarenou tridu * @param methodName * jmeno metody, jejiz struktura se ma vypsat * @throws NotFoundException * vyhozena, pokud metoda nebyla nalezena * @throws BadBytecode * vyhozena, pokud se nalezne neplatna instrukce v bytekodu */ private static void printMethodStructure(CtClass generatedClass, String methodName) throws NotFoundException, BadBytecode { System.out.println("Method '" + methodName + "' structure:"); CtMethod method = generatedClass.getDeclaredMethod(methodName); if (method == null) { System.out.println(" not found!"); return; } MethodInfo methodInfo = method.getMethodInfo(); System.out.println(" real name: " + methodInfo.getName()); System.out.println(" descriptor: " + methodInfo.getDescriptor()); System.out.println(" access flags: " + Modifier.toString(methodInfo.getAccessFlags())); System.out.println(" method body:"); printMethodBody(methodInfo); System.out.println(); } /** * Vypis instrukci tvoricich telo vybrane metody. * * @throws NotFoundException * vyhozena, pokud metoda nebyla nalezena * @throws BadBytecode * vyhozena, pokud se nalezne neplatna instrukce v bytekodu */ private static void printMethodBody(MethodInfo methodInfo) throws BadBytecode { CodeAttribute ca = methodInfo.getCodeAttribute(); CodeIterator iterator = ca.iterator(); while (iterator.hasNext()) { int index = iterator.next(); int opcode = iterator.byteAt(index); int nextIndex = iterator.lookAhead(); int spaces = 16; for (int i = index; i < nextIndex; i++) { System.out.format("%02x ", iterator.byteAt(i)); spaces-=3; } for (int i = 0; i < spaces; i++) { System.out.print(' '); } System.out.println(" " + Mnemonic.OPCODE[opcode]); } } /** * Vypis struktury vybranych metod z generovane tridy. * * @param generatedClass * predstavuje vytvarenou tridu * @throws NotFoundException * vyhozena, pokud metoda nebyla nalezena * @throws BadBytecode */ private static void printMethodStructures(CtClass generatedClass) throws NotFoundException, BadBytecode { printMethodStructure(generatedClass, "main"); printMethodStructure(generatedClass, "loopTest1"); printMethodStructure(generatedClass, "loopTest2"); } /** * Spusteni generatoru tridy. * * @param args nevyuzito */ public static void main(String[] args) { System.out.println("class generation begin: " + GENERATED_CLASS_NAME); try { CtClass generatedClass = generateClass(); // dulezite - generovana trida nesmi byt "zmrazena" generatedClass.defrost(); printMethodStructures(generatedClass); } catch (CannotCompileException e) { e.printStackTrace(); } catch (NotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (BadBytecode e) { e.printStackTrace(); } System.out.println("class generation end: " + GENERATED_CLASS_NAME); } }
9. Výstup demonstračního příkladu ClassGenerationTest9
Podívejme se nyní na to, jak vypadá výstup dnešního demonstračního příkladu ClassGenerationTest9. Asi nejzajímavější informací je způsob uložení cílů skoků ihned za operačními kódy instrukcí goto a if_icmplt. Povšimněte si, že se skutečně jedná o dvoubajtovou celočíselnou hodnotu se znaménkem. To například znamená, že sekvence bajtů a7 00 0e značí nepodmíněný skok (goto) o čtrnáct bajtů směrem dopředu, zatímco sekvence bajtů a1 ff f2 je podmíněný skok (konkrétně if_icmplt) o čtrnáct bajtů směrem dozadu (oba offsety jsou vypočteny od začátku příslušné instrukce):
class generation begin: GeneratedClass9 Method 'main' structure: real name: main descriptor: ([Ljava/lang/String;)V access flags: public static method body: b8 00 21 invokestatic b8 00 23 invokestatic b1 return Method 'loopTest1' structure: real name: loopTest1 descriptor: ()V access flags: public static method body: 03 iconst_0 3b istore_0 a7 00 0e goto b2 00 0e getstatic 12 10 ldc b6 00 16 invokevirtual 84 00 01 iinc 1a iload_0 10 0a bipush a1 ff f2 if_icmplt b1 return Method 'loopTest2' structure: real name: loopTest2 descriptor: ()V access flags: public static method body: 03 iconst_0 3b istore_0 a7 00 0e goto b2 00 1b getstatic 12 1d ldc b6 00 16 invokevirtual 84 00 01 iinc 1a iload_0 10 0a bipush a1 ff f2 if_icmplt b1 return class generation end: GeneratedClass9
10. Bajtkód třídy GeneratedClass9
Podívejme se nyní na způsob, jakým je bajtkód třídy GeneratedClass9 vypsán standardním nástrojem javap. Vzhledem k tomu, že budeme chtít vidět výpis bajtkódu jednotlivých metod, a to včetně metod soukromých, je nutné nástroj javap spustit následujícím způsobem:
javap -c -private GeneratedClass9
Nástroj javap nám však ve své současné verzi nedokáže vypsat i hexadecimální hodnoty bajtů tvořících operační kódy a operandy instrukcí, takže u skoků budeme vidět pouze absolutní adresy (samozřejmě platné v rámci těla metody):
Compiled from "GeneratedClass9.java" public class GeneratedClass9 extends java.lang.Object{ public static void loopTest1(); Code: 0: iconst_0 1: istore_0 2: goto 16 5: getstatic #14; //Field java/lang/System.out:Ljava/io/PrintStream; 8: ldc #16; //String Hello 10: invokevirtual #22; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 13: iinc 0, 1 16: iload_0 17: bipush 10 19: if_icmplt 5 22: return public static void loopTest2(); Code: 0: iconst_0 1: istore_0 2: goto 16 5: getstatic #27; //Field java/lang/System.err:Ljava/io/PrintStream; 8: ldc #29; //String world! 10: invokevirtual #22; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 13: iinc 0, 1 16: iload_0 17: bipush 10 19: if_icmplt 5 22: return public static void main(java.lang.String[]); Code: 0: invokestatic #33; //Method loopTest1:()V 3: invokestatic #35; //Method loopTest2:()V 6: return public GeneratedClass9(); Code: 0: aload_0 1: invokespecial #38; //Method java/lang/Object."":()V 4: return }
11. Repositář se zdrojovými kódy dnešního demonstračního příkladu ClassGenerationTest9
Následuje – v tomto seriálu již tradiční – kapitola s odkazy na zdrojové kódy. Dnes popsaný demonstrační příklad ClassGenerationTest9 je uložen do Mercurial repositáře dostupného na adrese http://icedtea.classpath.org/people/ptisnovs/jvm-tools/. V následující tabulce najdete odkazy na prozatím nejnovější verze tohoto zdrojového kódu:
# | Zdrojový soubor/skript | Umístění souboru v repositáři |
---|---|---|
1 | ClassGenerationTest9.java | http://icedtea.classpath.org/people/ptisnovs/jvm-tools/file/c4872d13d7c1/javassist/ClassGenerationTest9/ClassGenerationTest9.java |
12. Odkazy na Internetu
- GOTO instruction
http://www.vmth.ucdavis.edu/incoming/Jasmin/ref-_goto.html - Open Source ByteCode Libraries in Java
http://java-source.net/open-source/bytecode-libraries - IF_ICMPLT instruction
http://www.vmth.ucdavis.edu/incoming/Jasmin/ref–29.html - IFEQ instruction
http://www.vmth.ucdavis.edu/incoming/Jasmin/ref-_ifeq.html - ASM Home page
http://asm.ow2.org/ - Seznam nástrojů využívajících projekt ASM
http://asm.ow2.org/users.html - ObjectWeb ASM (Wikipedia)
http://en.wikipedia.org/wiki/ObjectWeb_ASM - Java Bytecode BCEL vs ASM
http://james.onegoodcookie.com/2005/10/26/java-bytecode-bcel-vs-asm/ - BCEL Home page
http://commons.apache.org/bcel/ - Byte Code Engineering Library (před verzí 5.0)
http://bcel.sourceforge.net/ - Byte Code Engineering Library (verze >= 5.0)
http://commons.apache.org/proper/commons-bcel/ - BCEL Manual
http://commons.apache.org/bcel/manual.html - Byte Code Engineering Library (Wikipedia)
http://en.wikipedia.org/wiki/BCEL - BCEL Tutorial
http://www.smfsupport.com/support/java/bcel-tutorial!/ - Bytecode Engineering
http://book.chinaunix.net/special/ebook/Core_Java2_Volume2AF/0131118269/ch13lev1sec6.html - Bytecode Outline plugin for Eclipse (screenshoty + info)
http://asm.ow2.org/eclipse/index.html - Javassist
http://www.jboss.org/javassist/ - Byteman
http://www.jboss.org/byteman - Java programming dynamics, Part 7: Bytecode engineering with BCEL
http://www.ibm.com/developerworks/java/library/j-dyn0414/ - The JavaTM Virtual Machine Specification, Second Edition
http://java.sun.com/docs/books/jvms/second_edition/html/VMSpecTOC.doc.html - The class File Format
http://java.sun.com/docs/books/jvms/second_edition/html/ClassFile.doc.html - javap – The Java Class File Disassembler
http://docs.oracle.com/javase/1.4.2/docs/tooldocs/windows/javap.html - javap-java-1.6.0-openjdk(1) – Linux man page
http://linux.die.net/man/1/javap-java-1.6.0-openjdk - Using javap
http://www.idevelopment.info/data/Programming/java/miscellaneous_java/Using_javap.html - Examine class files with the javap command
http://www.techrepublic.com/article/examine-class-files-with-the-javap-command/5815354 - aspectj (Eclipse)
http://www.eclipse.org/aspectj/ - Aspect-oriented programming (Wikipedia)
http://en.wikipedia.org/wiki/Aspect_oriented_programming - AspectJ (Wikipedia)
http://en.wikipedia.org/wiki/AspectJ - EMMA: a free Java code coverage tool
http://emma.sourceforge.net/ - Cobertura
http://cobertura.sourceforge.net/ - jclasslib bytecode viewer
http://www.ej-technologies.com/products/jclasslib/overview.html