Hlavní navigace

Pohled pod kapotu JVM - nastavení breakpointů s využitím rozhraní JVM TI

12. 2. 2013
Doba čtení: 14 minut

Sdílet

Dnes se začneme zabývat další velmi důležitou funkcí nabízenou rozhraním JVM TI. Jedná se o funkci určenou pro nastavení breakpointů (bodů přerušení). Breakpoint je možné nastavit v libovolné metodě na zvolenou instrukci, takže v JVM TI je práce s breakpointy přesnější než ve většině debuggerů či IDE.

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

10. Odkazy na Internetu

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:

  1. Získat identifikátor metody, pro niž se breakpoint nastavuje.
  2. Získat lokaci (umístění) breakpointu v rámci metody (většinou na základě čísla řádku).
  3. Provést registraci breakpointu s využitím funkceSetBreakpoint().
  4. 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.or­g/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:

CS24_early

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:

  1. Zjistit hodnotu typu jmethodID pro metodu, v níž budeme chtít zaregistrovat breakpoint.
  2. 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.
  3. Nastavit požadované schopnosti agenta, zejména povolit generování callback funkcí při průchodu breakpointem.
  4. Nastavit breakpoint funkcí SetBreakpoint() na základě hodnot jmethodID a jlocation zjištěných v bodech 1 a 2.
  5. 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

  1. Breakpoint (Wikipedia)
    http://cs.wikipedia.org/wi­ki/Breakpoint
  2. JVM Tool Interface Version 1.2 Documentation
    http://docs.oracle.com/ja­vase/7/docs/platform/jvmti/jvmti­.html
  3. JVM Tool Interface Version 1.2 Documentation – SetBreakpoint
    http://docs.oracle.com/ja­vase/6/docs/platform/jvmti/jvmti­.html#SetBreakpoint
  4. JVM Tool Interface Version 1.2 Documentation – ClearBreakpoint
    http://docs.oracle.com/ja­vase/6/docs/platform/jvmti/jvmti­.html#ClearBreakpoint
  5. JVM Tool Interface Version 1.2 Documentation – Breakpoint (callback funkce)
    http://docs.oracle.com/ja­vase/6/docs/platform/jvmti/jvmti­.html#Breakpoint
  6. The JVM Tool Interface (JVM TI): How VM Agents Work
    http://www.oracle.com/technet­work/articles/javase/jvm-ti-141370.html
  7. Creating a Debugging and Profiling Agent with JVMTI
    http://www.oracle.com/technet­work/articles/javase/jvmti-136367.html
  8. JVM TI (Wikipedia)
    http://en.wikipedia.org/wiki/JVM_TI
  9. IBM JVMTI extensions
    http://publib.boulder.ibm­.com/infocenter/realtime/v2r0/in­dex.jsp?topic=%2Fcom.ibm.sof­trt.doc%2Fdiag%2Ftools%2Fjvmti_ex­tensions.html
  10. An empirical study of Java bytecode programs
    http://www.mendeley.com/research/an-empirical-study-of-java-bytecode-programs/
  11. Java quick guide: JVM Instruction Set (tabulka všech instrukcí JVM)
    http://www.mobilefish.com/tu­torials/java/java_quickgu­ide_jvm_instruction_set.html
  12. The JVM Instruction Set
    http://mpdeboer.home.xs4a­ll.nl/scriptie/node14.html
  13. Control Flow in the Java Virtual Machine
    http://www.artima.com/under­thehood/flowP.html
  14. The class File Format
    http://java.sun.com/docs/bo­oks/jvms/second_edition/html/Clas­sFile.doc.html
  15. javap – The Java Class File Disassembler
    http://docs.oracle.com/ja­vase/1.4.2/docs/tooldocs/win­dows/javap.html
  16. javap-java-1.6.0-openjdk(1) – Linux man page
    http://linux.die.net/man/1/javap-java-1.6.0-openjdk
  17. Using javap
    http://www.idevelopment.in­fo/data/Programming/java/mis­cellaneous_java/Using_javap­.html
  18. Examine class files with the javap command
    http://www.techrepublic.com/ar­ticle/examine-class-files-with-the-javap-command/5815354
  19. Bytecode Engineering
    http://book.chinaunix.net/spe­cial/ebook/Core_Java2_Volu­me2AF/0131118269/ch13lev1sec6­.html

Byl pro vás článek přínosný?

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.