Obsah
1. Podpora práce s breakpointy v rozhraní JVM TI
2. JVM TI funkce SetBreakpoint() a ClearBreakpoint()
3. Callback funkce zavolaná ve chvíli dosažení breakpointu
4. Postup při nastavení breakpointů
5. Získání identifikátorů všech metod pro zvolenou třídu
6. Demonstrační agent číslo 30 – výpis všech metod pro zvolenou třídu
7. Spuštění třicátého demonstračního agenta
8. Zdrojové kódy demonstračního agenta i k němu příslušných testovacích příkladů a skriptů
9. Obsah následující části seriálu
1. Podpora práce s breakpointy v rozhraní JVM TI
V dnešní části seriálu o programovacím jazyce Java i o vlastnostech virtuálního stroje Javy se budeme zabývat popisem jedné z nejužitečnějších funkcí nabízených rozhraním JVM TI. Jedná se o funkci nazvanou SetBreakpoint() a v menší míře i o funkci ClearBreakpoint(). Jak již názvy těchto funkcí naznačují, lze je využít pro nastavení či naopak zrušení breakpointů vkládaných na určitá místa v bajtkódu. Breakpointů může být zaregistrováno libovolné množství a ve chvíli, kdy nějaké vlákno k zaregistrovanému breakpointu dojde, zavolá rozhraní JVM TI callback funkci společnou pro všechny nastavené breakpointy.
V této callback funkci je již možné získat všechny potřebné informace o místě, kde vlákno došlo k breakpointu, lze samozřejmě získat hodnoty atributů sledovaného objektu, výpis zásobníkových rámců platných pro aktuální vlákno atd. atd. Možnosti nabízené rozhraním JVM TI jsou v tomto ohledu dosti velké, i když je nutné říci, že pro implementaci plnohodnotného debuggeru může být jednodušší využít rozhraní JDWP (k popisu tohoto velmi zajímavého a užitečného rozhraní se v tomto seriálu dostaneme později).
Na tomto místě ještě stojí za zmínku si připomenout, že v předchozích částech tohoto seriálu jsme si popsali funkce, které lze využít pro trasování vstupu do metod a výstupu z metod:
void JNICALL MethodEntry( jvmtiEnv *jvmti_env, JNIEnv *jni_env, jthread thread, jmethodID method)
void JNICALL MethodExit( jvmtiEnv *jvmti_env, JNIEnv *jni_env, jthread thread, jmethodID method, jboolean was_popped_by_exception, jvalue return_value)
Tyto funkce mohou v některých případech nahradit breakpointy, ovšem mají jednu dosti zásadní nevýhodu – tyto callback funkce jsou totiž zaregistrovány globálně pro všechny metody, což mj. znamená, že se při jejich použití výrazně zpomalí běh virtuálního stroje Javy. U breakpointů toto nebezpečí nehrozí, samozřejmě v případě, že jich nebudeme mít zaregistrováno několik tisíc. Zajímavé taktéž je, že nastavené breakpointy zůstanou zachovány i tehdy, pokud je daná metoda přeložena JIT překladačem do nativního strojového kódu (ostatně algoritmus, který breakpointy zachovává i v tomto případě, je dosti složitý a v minulosti nebyl zcela bez chyb).
2. JVM TI funkce SetBreakpoint() a ClearBreakpoint()
Popišme si nyní obě funkce určené pro nastavování a rušení breakpointů. Pro nastavení breakpointu se používá funkce jvmti->SetBreakpoint(). Této funkci je nutné předat dva důležité údaje. Jedná se v první řadě o identifikátor metody, tj. o hodnotu typu jmethodID, kterou je nutné nějakým způsobem získat (jak, to se dozvíme v navazujících kapitolách). Druhým důležitým parametrem je parametr typu jlocation, který určuje přesné místo v rámci metody, kam má být breakpoint umístěn. Ve většině existujících virtuálních strojů Javy platí, že jlocation odpovídá indexům instrukcí bajtkódu v metodě, což znamená, že breakpoint lze nastavit s přesností na instrukci, zatímco většina (?) debuggerů a integrovaných vývojových prostředí pracuje s řádky zdrojového kódu. V praxi to znamená, že přes rozhraní JVM TI lze umístit breakpoint například doprostřed výrazu, což může být v některých případech užitečné.
Hlavička funkce jvmti->SetBreakpoint() vypadá následovně:
jvmtiError SetBreakpoint( jvmtiEnv* env, jmethodID method, jlocation location)
Zajímavé je, že funkce určená pro rušení existujícího breakpointu (jvmti->ClearBreakpoint()) má stejné parametry jako funkce jvmti->SetBreakpoint(). Je to vlastně pochopitelné, neboť i při rušení breakpointu je nutné specifikovat metodu a index instrukce v rámci této metody:
jvmtiError ClearBreakpoint( jvmtiEnv* env, jmethodID method, jlocation location)
Jak jsme si již řekli v předchozí kapitole, zůstává breakpoint nastaven i v případě, že je metoda přeložena JIT překladačem do nativního strojového kódu. U většiny virtuálních strojů Javy se však musí interně zakázat některé optimalizace, které by vedly například k eliminaci kódu, na němž je breakpoint nastaven. Teoreticky to tedy znamená, že nastavením breakpointu se běh programu zpomalí, ve skutečnosti se však většinou jedná o minimální rozdíly. Výjimkou by byly případy, kdy by byl breakpoint nastaven ve vnitřní smyčce nějakého výpočtu, který by jinak mohl být eliminován optimalizujícím JIT překladačem. Druhý problém může nastat u některých „alternativních“ JVM (například Zero+Shark pro ARM) ve chvíli, kdy se breakpoint nastavuje do metody s prázdným tělem – Shark totiž volání prázdných metod eliminuje i v případě, že je zde breakpoint nastaven (což by v ideálním případě mělo být ošetřeno).
3. Callback funkce zavolaná ve chvíli dosažení breakpointu
Nastavení breakpointu s využitím funkce jvmti->SetBreakpoint() však ještě nestačí k tomu, aby JVM TI agent dostával zprávy o tom, že nějaké vlákno breakpointu skutečně dosáhlo. Musíme totiž ještě implementovat uživatelskou callback funkci zavolanou ve chvíli dosažení breakpointu. Hlavička této callback funkce vypadá následovně:
void JNICALL Breakpoint( jvmtiEnv *jvmti_env, JNIEnv *jni_env, jthread thread, jmethodID method, jlocation location)
V následující tabulce je vypsán význam všech pěti parametrů této funkce. Tyto parametry jsou samozřejmě naplňovány virtuálním strojem Javy ve chvíli před zavoláním callback funkce:
# | Typ parametru | Jméno parametru | Význam |
---|---|---|---|
1 | jvmtiEnv * | jvmti_env | JVM TI prostředí agenta (je předáváno do většiny callback funkcí) |
2 | JNIEnv * | jni_env | JNI prostředí platné pro dané vlákno (je předáváno do některých callback funkcí) |
3 | jthread | thread | vlákno, v němž došlo k dosažení instrukce označené breakpointem |
4 | jmethodID | method | metoda s registrovaným breakpointem |
5 | jlocation | location | identifikace instrukce, na níž je nastavený registrovaný breakpoint |
4. Postup při nastavení breakpointů
V dalším kroku je nutné povolit generování událostí ve chvíli dosažení breakpointu. Podobně jako u dalších typů událostí, i zde se „povolovací sekvence“ rozděluje na dva kroky. Nejprve musíme přes JVM TI rozhraní oznámit, že danou funkcionalitu vyžadujeme, a to s využitím struktury jvmtiCapabilities a JVM TI funkce jvmti->AddCapabilities():
jvmtiCapabilities capabilities; memset(&capabilities, 0, sizeof(jvmtiCapabilities)); capabilities.can_generate_breakpoint_events = 1; (*jvmti)->AddCapabilities(jvmti, &capabilities);
Následně je nutné nastavit režim notifikace s využitím funkce jvmti->SetEventNotificationMode(), které se předá hodnota JVMTI_EVENT_BREAKPOINT:
(*jvmti)->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_BREAKPOINT, NULL)
Následně musíme provést tyto kroky:
- Získat identifikátor metody, pro niž se breakpoint nastavuje.
- Získat lokaci (umístění) breakpointu v rámci metody (většinou na základě čísla řádku).
- Provést registraci breakpointu s využitím funkceSetBreakpoint().
- Definovat uživatelskou callback funkci popsanou v předchozí kapitole.
5. Získání identifikátorů všech metod pro zvolenou třídu
V předchozích kapitolách jsme si řekli, že pro zdárné zaregistrování nového breakpointu je nutné funkci SetBreakpoint() předat identifikátor metody a umístění breakpointu v rámci této metody. Nejprve se podívejme na to, jakým způsobem se zjistí všechny metody zvolené třídy. Ve skutečnosti je to velmi jednoduché, protože můžeme využít postup, s nímž jsme se seznámili v předchozích dvou dílech tohoto seriálu – využijeme uživatelskou callback funkci callback_on_class_prepare() volanou v době, kdy jsou do virtuálního stroje Javy načítány jednotlivé třídy. Pro každou třídu zjistíme pomocí funkce GetClassSignature() její jméno a pokud se jedná o testovací třídu (resp. třídu, pro jejíž metodu má být nastaven breakpoint), zavolá se uživatelská funkce list_all_methods(), která vypíše jména všech metod deklarovaných v rámci nalezené třídy:
/* * Callback funkce zavolana ve chvili, kdy je trida ve virtualnim stroji ve stavu, * kdy ji lze normalne pouzivat. */ static void JNICALL callback_on_class_prepare( jvmtiEnv *jvmti_env, JNIEnv *jni_env, jthread thread, jclass class) { jvmtiError error; char *class_name_ptr; char *updated_class_name_ptr; enter_critical_section(jvmti_env); /* ziskat jmeno tridy */ error = (*jvmti_env)->GetClassSignature(jvmti_env, class, &class_name_ptr, NULL); check_jvmti_error(jvmti_env, error, "get class signature"); if (class_name_ptr == NULL) { puts("Error: class has no signature"); } /* upravit jmeno tridy */ updated_class_name_ptr = update_class_name(class_name_ptr, ';'); /* pokud jsme nasli to pravou tridu */ if (strcmp(updated_class_name_ptr, TEST_CLASS_NAME) == 0) { puts("Class "TEST_CLASS_NAME" prepared, listing all its methods"); list_all_methods(jvmti_env, class); } /* dealokace pameti po GetClassSignature() */ error = (*jvmti_env)->Deallocate(jvmti_env, (unsigned char *)class_name_ptr); check_jvmti_error(jvmti_env, error, "deallocate class name"); exit_critical_section(jvmti_env); }
Uživatelské funkci list_all_methods() je předán identifikátor třídy představovaný hodnotou typu jclass. Pro získání seznamu všech metod této třídy se použije funkce GetClassMethods(), která vrátí pole obsahující prvky typu jmethodID a taktéž vrátí délku tohoto pole. Pro každý prvek typu jmethodID lze již snadno získat jméno i signaturu metody, a to s využitím JVM TI funkce GetMethodName(). Po využití jména i signatury samozřejmě nesmíme zapomenout na dealokaci obou řetězců, což zajistí nám již dobře známá funkce Deallocate():
/* * Vypis vsech metod zvolene tridy. */ void list_all_methods(jvmtiEnv *jvmti_env, jclass class) { jvmtiError error; int method_count; jmethodID *methods_array; /* precist vsechny metody tridy */ error = (*jvmti_env)->GetClassMethods(jvmti_env, class, &method_count, &methods_array); check_jvmti_error(jvmti_env, error, "get class methods"); /* pole metod bylo inicializovano */ if (error == JVMTI_ERROR_NONE) { int i; /* projit vsemi metodami */ for (i = 0; i < method_count; i++) { char *method_name; char *method_signature; jmethodID method = methods_array[i]; error = (*jvmti_env)->GetMethodName(jvmti_env, method, &method_name, &method_signature, NULL); if (error == JVMTI_ERROR_NONE) { printf("Found method(): %s with signature %s\n", method_name, method_signature); (*jvmti_env)->Deallocate(jvmti_env, (unsigned char*)method_name); (*jvmti_env)->Deallocate(jvmti_env, (unsigned char*)method_signature); } } } /* dealokace pole ziskaneho pres GetClassFields() */ error = (*jvmti_env)->Deallocate(jvmti_env, (unsigned char*)methods_array); check_jvmti_error(jvmti_env, error, "deallocate class fields array"); }
6. Demonstrační agent číslo 30 – výpis všech metod pro zvolenou třídu
Výše popsané uživatelské funkce callback_on_class_prepare() a list_all_methods() tvoří ústřední prvek již třicátého demonstračního JVM TI agenta, který po svém spuštění v rámci JVM vypíše seznam všech metod testovací třídy Test30. U každé metody je vypsáno jak její jméno (které nemusí být jednoznačné), tak i signatura metody, z níž lze nám již známým postupem vyčíst počet a typ parametrů metody i typ návratové hodnoty této metody. Testovací třída Test30 mj. obsahuje i několik metod se shodným názvem x, které se však liší (=musí se lišit) svými signaturami:
/** * Testovaci trida pouzite pro test tricateho * demonstracniho JVM TI agenta. */ public class Test30 { public void foo() { System.out.println("Test30.foo()"); bar(); } public void bar() { System.out.println("Test30.bar() line 12"); System.out.println("Test30.bar() line 13"); System.out.println("Test30.bar() line 14"); System.out.println("Test30.bar() line 15"); } public void run() { System.out.println("Test30.run()"); foo(); } public int x() {return 0;} public int x(int y) {return 0;} public int x(int y, boolean z) {return 0;} public int[] x(int[] y, double[][] z) {return null;} /** * Spusteni testu. */ public static void main(String[] args) { new Test30().run(); } }
7. Spuštění třicátého demonstračního agenta
Demonstrační JVM TI agent i k němu příslušná testovací třída Test30 se přeloží stejným způsobem, jaký jsme použili i u všech předchozích příkladů:
gcc -Wall -ansi -I/usr/lib/jvm/java-1.6.0-openjdk/include/ -shared -o libagent30.so agent30.c javac -g Test30.java
O spuštění virtuálního stroje Javy společně s JVM TI agentem se postará příkaz:
java -agentpath:./libagent30.so Test30 2> /dev/null
Po spuštění by se měl na standardním výstupu objevit mj. i seznam všech metod testovací třídy Test30, a to včetně signatur těchto metod:
Test30.run() Test30.foo() Test30.bar() line 12 Test30.bar() line 13 Test30.bar() line 14 Test30.bar() line 15 Agent30: Agent_OnLoad Agent30: JVM TI version is correct Agent30: Got VM init event Class Test30; prepared, listing all its methods Found method(): <init> with signature ()V Found method(): main with signature ([Ljava/lang/String;)V Found method(): run with signature ()V Found method(): x with signature ()I Found method(): x with signature (I)I Found method(): x with signature (IZ)I Found method(): x with signature ([I[[D)[I Found method(): foo with signature ()V Found method(): bar with signature ()V Agent30: Got VM Death event Agent30: Agent_OnUnload
8. Zdrojové kódy demonstračního agenta i k němu příslušných testovacích příkladů a skriptů
Podobně jako v mnoha předcházejících částech tohoto seriálu byl i dnešní demonstrační JVM TI agent kvůli snazšímu udržování všech zdrojových kódů uložen do Mercurial repositáře, který je dostupný na adrese http://icedtea.classpath.org/people/ptisnovs/jvm-tools/. Prozatím nejnovější verze dnes popisovaného JVM TI agenta i dalších potřebných skriptů a testovacích javovských tříd jsou dostupné na následujících adresách:
Demonstrační příklad/podpůrný soubor | Umístění |
---|---|
Agent #30 | http://icedtea.classpath.org/people/ptisnovs/jvm-tools/file/2a7ebf111840/jvmti-agents/agent30/agent30.c |
Skript pro překlad agenta #30 | http://icedtea.classpath.org/people/ptisnovs/jvm-tools/file/2a7ebf111840/jvmti-agents/agent30/compile.sh |
Skript pro spuštění agenta #30 | http://icedtea.classpath.org/people/ptisnovs/jvm-tools/file/2a7ebf111840/jvmti-agents/agent30/test.sh |
Testovací třída Test30.java | http://icedtea.classpath.org/people/ptisnovs/jvm-tools/file/2a7ebf111840/jvmti-agents/agent30/Test30.java |
9. Obsah následující části seriálu
První část našeho problému již vlastně máme vyřešenou – na základě jména třídy a jména metody totiž dokážeme získat hodnotu typu jmethodID, která vybranou metodu jednoznačně reprezentuje v rámci spuštěného virtuálního stroje Javy. Můžeme se tedy pustit do přípravy 31 demonstračního JVM TI agenta, v němž bude nutné provést tyto operace:
- Zjistit hodnotu typu jmethodID pro metodu, v níž budeme chtít zaregistrovat breakpoint.
- Převést číslo řádku na hodnotu typu jlocation. Číslo řádku přitom odpovídá pořadí řádku v textovém souboru se zdrojovým kódem třídy.
- Nastavit požadované schopnosti agenta, zejména povolit generování callback funkcí při průchodu breakpointem.
- Nastavit breakpoint funkcí SetBreakpoint() na základě hodnot jmethodID a jlocation zjištěných v bodech 1 a 2.
- Zaregistrovat callback funkci zavolanou při průchodu breakpointem (viz téžtřetí kapitolu).
Všechny tyto kroky si podrobně popíšeme v navazující části tohoto seriálu. Zde si jen jako malou ukázku uvedeme zprávy, které agent po svém spuštění vypíše na standardní výstup:
Test31.run() Test31.foo() Test31.bar() line 1 Test31.bar() line 2 Test31.bar() line 3 Test31.bar() line 4 Agent31: Agent_OnLoad Agent31: JVM TI version is correct Agent31: Got VM init event Class Test31; prepared, setting breakpoint for method bar Found method(): <init> with signature ()V Found method(): main with signature ([Ljava/lang/String;)V Found method(): run with signature ()V Found method(): x with signature ()I Found method(): x with signature (I)I Found method(): x with signature (IZ)I Found method(): x with signature ([I[[D)[I Found method(): foo with signature ()V Found method(): bar with signature ()V ...going to set breakpoint for this method *** breakpoint visited!!! *** Agent31: Got VM Death event Agent31: Agent_OnUnload
10. Odkazy na Internetu
- Breakpoint (Wikipedia)
http://cs.wikipedia.org/wiki/Breakpoint - JVM Tool Interface Version 1.2 Documentation
http://docs.oracle.com/javase/7/docs/platform/jvmti/jvmti.html - JVM Tool Interface Version 1.2 Documentation – SetBreakpoint
http://docs.oracle.com/javase/6/docs/platform/jvmti/jvmti.html#SetBreakpoint - JVM Tool Interface Version 1.2 Documentation – ClearBreakpoint
http://docs.oracle.com/javase/6/docs/platform/jvmti/jvmti.html#ClearBreakpoint - JVM Tool Interface Version 1.2 Documentation – Breakpoint (callback funkce)
http://docs.oracle.com/javase/6/docs/platform/jvmti/jvmti.html#Breakpoint - The JVM Tool Interface (JVM TI): How VM Agents Work
http://www.oracle.com/technetwork/articles/javase/jvm-ti-141370.html - Creating a Debugging and Profiling Agent with JVMTI
http://www.oracle.com/technetwork/articles/javase/jvmti-136367.html - JVM TI (Wikipedia)
http://en.wikipedia.org/wiki/JVM_TI - IBM JVMTI extensions
http://publib.boulder.ibm.com/infocenter/realtime/v2r0/index.jsp?topic=%2Fcom.ibm.softrt.doc%2Fdiag%2Ftools%2Fjvmti_extensions.html - An empirical study of Java bytecode programs
http://www.mendeley.com/research/an-empirical-study-of-java-bytecode-programs/ - Java quick guide: JVM Instruction Set (tabulka všech instrukcí JVM)
http://www.mobilefish.com/tutorials/java/java_quickguide_jvm_instruction_set.html - The JVM Instruction Set
http://mpdeboer.home.xs4all.nl/scriptie/node14.html - Control Flow in the Java Virtual Machine
http://www.artima.com/underthehood/flowP.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 - Bytecode Engineering
http://book.chinaunix.net/special/ebook/Core_Java2_Volume2AF/0131118269/ch13lev1sec6.html