Hlavní navigace

Pohled pod kapotu JVM - nastavení breakpointů s využitím rozhraní JVM TI (pokračování)

19. 2. 2013
Doba čtení: 15 minut

Sdílet

Dnes budeme společnými silami pokračovat v popisu způsobu nastavování breakpointů s využitím rozhraní JVM TI. Na příkladu demonstračního JVM TI agenta si ukážeme, jak lze zaregistrovat breakpoint pro zvolenou metodu a číslo řádku i to, jakým způsobem je agent informován a vstupu na breakpoint.

Obsah

1. Demonstrační agent číslo 31 – nastavení breakpointu a výpis zprávy při přístupu na breakpoint

2. Příprava pro nastavení breakpointu

3. Nastavení breakpointu pro zvolenou metodu a číslo řádku

4. Převod čísla řádku na index instrukce

5. Registrace callback funkce zavolané při vstupu na breakpoint

6. Testovací třída použitá společně s demonstračním agentem číslo 31

7. Spuštění třicátého prvního demonstračního JVM TI agenta

8. Zdrojové kódy demonstračního agenta i k němu příslušných testovacích příkladů a skriptů

9. Odkazy na Internetu

1. Demonstrační agent číslo 31 – nastavení breakpointu a výpis zprávy při přístupu na breakpoint

V předchozí části seriálu o programovacím jazyce Java i o vlastnostech virtuálního stroje Javy jsme si řekli základní informace o tom, jakým způsobem je možné s využitím rozhraní JVM TI nastavit breakpoint a následně zaregistrovat callback funkci zavolanou ve chvíli, kdy libovolné vlákno v aplikaci přistoupí na některý z nastavených breakpointů (těch může být samozřejmě zaregistrováno libovolné množství). Připomeňme si, že pro nastavení breakpointu se využívá funkce nazvaná jvmti->SetBreakpoint() a v případě, že potřebujeme některý dříve nastavený breakpoint zrušit, zavolá se funkce jvmti->ClearBreakpoint(). 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 zavolané 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.

Oběma zmíněným funkcím jvmti->SetBreakpoint() a jvmti->ClearBreakpoint() je nutné předat dva důležité údaje, které rozhraní JVM TI využije pro nastavení popř. naopak pro zrušení breakpointu (hlavičky obou funkcí jsou shodné). Jedná se v první řadě o jednoznačný identifikátor metody, tj. o hodnotu typu jmethodID, kterou je nutné nějakým způsobem získat (jeden ze způsobů jsme si ukázali minule a vrátíme se k němu i v dnešním demonstračním příkladu). Druhým důležitým parametrem předávaným do funkce jvmti->SetBreakpoint() i do jvmti->ClearBreakpoint() 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 „strojovou“ instrukci, zatímco většina (?) debuggerů a integrovaných vývojových prostředí pracuje s řádky zdrojového kódu.

Posledním důležitým údajem zmíněným minule je informace o hlavičce callback funkce zaregistrované a zavolané ve chvíli, kdy nějaké vlákno aplikace dosáhne breakpointu. Této callback funkci se mj. předá i informace o vláknu, které breakpointu dosáhlo, identifikátor metody s breakpointem a přesná lokace breakpointu v rámci této metody. Díky těmto informacím je zajištěno, že v JVM TI agentovi může být zaregistrována pouze tato jediná callback funkce (jak se registruje, si řekneme za chvíli), která bude využitelná pro všechny breakpointy. Hlavička této callback funkce vypadá následovně:

void JNICALL Breakpoint(
            jvmtiEnv *jvmti_env,
            JNIEnv   *jni_env,
            jthread   thread,
            jmethodID method,
            jlocation location)

2. Příprava pro nastavení breakpointu

Nyní si pojďme říci, jakou přípravu je nutné udělat při nastavení breakpointu pro zvolenou metodu. První část našeho problému jsme již vlastně vyřešili minule – na základě jména třídy a jména metody dokážeme relativně jednoduše 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 dnešního (celkově již třicátého prvního!) demonstračního JVM TI agenta, v němž se nejprve zaregistruje breakpoint a po přístupu k tomuto breakpointu se zavolá uživatelská callback funkce, která vypíše informaci o návštěvě breakpointu.

Nejprve je nutné nastavit požadované schopnosti agenta, kde si vyžádáme možnost generovat události při průchodu breakpointem:

/*
 * Nastaveni pozadovanych schopnosti agenta.
 */
jvmtiError set_capabilities(jvmtiEnv *jvmti)
{
    jvmtiCapabilities capabilities;
    jvmtiError error_code;
 
    memset(&capabilities, 0, sizeof(jvmtiCapabilities));
 
    /* vyuzivame tri specialni schopnosti agenta */
    capabilities.can_get_line_numbers = 1;
    capabilities.can_get_source_file_name = 1;
    capabilities.can_generate_breakpoint_events = 1;
 
    error_code = (*jvmti)->AddCapabilities(jvmti, &capabilities);
    check_jvmti_error(jvmti, error_code, "Unable to get necessary JVMTI capabilities.");
    return error_code;
}

Dále použijeme upravenou variantu funkce callback_on_class_prepare(), kde již nebude docházet pouze k výpisu všech metod testovací třídy, ale v případě, že se nalezne třída, pro jejíž metodu se má nastavit breakpoint, dojde k zavolání další uživatelské funkce prepare_breakpoint():

/*
 * 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, setting breakpoint for method "BREAKPOINT_FOR_METHOD);
        prepare_breakpoint(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);
}

3. Nastavení breakpointu pro zvolenou metodu a číslo řádku

V uživatelské funkci prepare_breakpoint() nejprve již popsaným postupem získáme seznam (resp. přesněji řečeno se jedná o céčkové pole) všech metod vybrané třídy a v případě, že nalezneme metodu, pro níž se má breakpoint nastavit, dojde k zavolání třetí (již téměř poslední) uživatelské funkce nazvané set_breakpoint(), které se předá identifikátor metody typu jmethodID. Test, zda jsme skutečně našli metodu, pro níž se má breakpoint nastavit, je v našem demonstračním příkladu velmi jednoduchý, protože pouze porovnáváme jméno metody s názvem uloženým v konstantě BREAKPOINT_FOR_METHOD. V praxi by byla situace poněkud složitější, protože v jedné třídě se samozřejmě může nacházet větší množství metod se stejným jménem, ale odlišnou signaturou. Korektnější by tedy bylo porovnávat i signatury metod:

/*
 * Registrace breakpointu pro zvolenou metodu.
 */
void prepare_breakpoint(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 a nastavit breakpoint pro zvolenou metodu */
        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);
                if (strcmp(method_name, BREAKPOINT_FOR_METHOD) == 0)
                {
                    puts("...going to set breakpoint for this method");
                    set_breakpoint(jvmti_env, method, BREAKPOINT_LINE_NUMBER);
                }
                (*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");
}

Funkce set_breakpoint() zavolaná z výše popsané funkce prepare_breakpoint(), je ve skutečnosti velmi jednoduchá – nejprve zjistí hodnotu jlocation a posléze pouze zavolá JVM TI funkci SetBreakpoint() se všemi vyžadovanými parametry:

/*
 * Nastaveni breakpointu pro zvolenou metodu na danem radku.
 */
void set_breakpoint(jvmtiEnv *jvmti_env, jmethodID method, int line)
{
    jvmtiError  error;
    jlocation   location = get_location_for_line(jvmti_env, method, line);
 
    error = (*jvmti_env)->SetBreakpoint(jvmti_env, method, location);
    check_jvmti_error(jvmti_env, error, "set breakpoint");
}

4. Převod čísla řádku na index instrukce

Jak jste si zajisté v předchozí kapitole všimli, volali jsme z uživatelské funkce set_breakpoint() další – prozatím neznámou – uživatelskou funkci nazvanou get_location_for_line(). Tato funkce převádí číslo řádku (kladné celé číslo) na hodnotu typu jlocation s využitím JVM TI funkce jvmti->GetLineNumberTable(). Tento převod se provádí přesně opačným způsobem, než tomu bylo ve funkci get_line_number(), jíž jsme použili například ve dvacátém devátém demonstračním JVM TI agentovi popsaném v předminulé části tohoto seriálu.

Rozdíl mezi oběma zmíněnými funkcemi spočívá v tom, jakým způsobem se pracuje s tabulkou obsahující záznamy typu jvmtiLineNumberEntry – zde totiž pro zadané číslo řádku vyhledáme index první instrukce bajtkódu odpovídajícímu zdrojovému řádku (pro jednoduchost předpokládejme, že zdrojový řádek skutečně obsahuje přeložitelný kód).

Kód uživatelské funkce get_location_for_line() může v nejjednodušším případě vypadat následovně:

/*
 * Ziskani indexu instrukce pro dane cislo radku.
 */
jlocation get_location_for_line(jvmtiEnv *jvmti_env, jmethodID method, int line)
{
    int count;
    int i;
    jvmtiLineNumberEntry *location_table;
    jvmtiError error_code;
    /* pokud se číslo řádku nenalezne nebo zde není žádný kód, vrátí se chybová hodnota -1 */
    jlocation  location = -1;
 
    /* pokud nebyla předána žádná metoda */
    if (method == NULL)
    {
        return -1;
    }
 
    /* test na korektně zadané číslo řádku */
    if (line <= 0)
    {
        return -1;
    }
 
    /* nacteni tabulky s cisly radku a indexy instrukci */
    error_code = (*jvmti_env)->GetLineNumberTable(jvmti_env, method, &count, &location_table);
    /* v nekterych pripadech se nacist tabulku nemusi podarit */
    if (error_code != JVMTI_ERROR_NONE)
    {
        return -1;
    }
 
    /* projit celou tabulkou */
    for (i = 0; i < count; i++)
    {
        jvmtiLineNumberEntry entry = location_table[i];
        if (entry.line_number == line)
        {
            location = entry.start_location;
            break;
        }
    }
 
    /* nesmíme zapomenout na dealokaci tabulky (pole) s dvojicemi číslo řádku:location */
    (*jvmti_env)->Deallocate(jvmti_env, (unsigned char *)location_table);
    return location;
}

Nesmíme zapomenout pouze na jednu drobnost – číslo řádku se neuvádí relativně vůči začátku metody, ale jedná se o číslo platné v rámci zdrojového kódu třídy, tedy o stejné číslo, které se zobrazí v textovém editoru. Taktéž je nutné zajistit, aby JVM TI agent měl nastavenou vlastnosti can_get_line_numbers (což je již zařízeno ve funkci set_capabilities()).

5. Registrace callback funkce zavolané při vstupu na breakpoint

Aby se skutečně zavolala callback funkce při průchodu nějakého vlákna zaregistrovaným breakpointem, je nutné nastavit notifikační režim JVMTI_EVENT_BREAKPOINT, což se již tradičně provádí v uživatelské funkci nazvané set_event_notification_modes():

/*
 * Nastaveni udalosti, pro nez se maji zavolat callback funkce.
 */
jvmtiError set_event_notification_modes(jvmtiEnv *jvmti)
{
    jvmtiError error_code;
 
    /* Potrebujeme zachytavat udalost inicializace virtualniho stroje. */
    if ((error_code = set_event_notification_mode(jvmti, JVMTI_EVENT_VM_INIT)) != JNI_OK)
    {
        return error_code;
    }
 
    /* Potrebujeme zachytavat udalost ukonceni prace virtualniho stroje. */
    if ((error_code = set_event_notification_mode(jvmti, JVMTI_EVENT_VM_DEATH)) != JNI_OK)
    {
        return error_code;
    }
 
    /* Udalost pri priprave tridy ve virtualnim stroji. */
    if ((error_code = set_event_notification_mode(jvmti, JVMTI_EVENT_CLASS_PREPARE)) != JNI_OK)
    {
        return error_code;
    }
 
    /* Udalost pri vstupu na radek s breakpointem. */
    if ((error_code = set_event_notification_mode(jvmti, JVMTI_EVENT_BREAKPOINT)) != JNI_OK)
    {
        return error_code;
    }
 
    return error_code;
}

Registrace nové callback funkce nazvané příznačně callback_on_breakpoint() se provede v uživatelské funkci register_all_callback_functions(), kterou již známe z naprosté většiny až doposud popsaných demonstračních JVM TI agentů. Povšimněte si toho, že ukazatel na tuto callback funkci se ukládá do struktury jvmtiEventCallbacks.Breakpoint:

/*
 * Registrace ctyr callback funkci zavolanych virtualnim strojem javy.
 */
jvmtiError register_all_callback_functions(jvmtiEnv *jvmti)
{
    jvmtiEventCallbacks callbacks;
    jvmtiError error_code;
 
    memset(&callbacks, 0, sizeof(callbacks));
 
    /* JVMTI_EVENT_VM_INIT */
    callbacks.VMInit = &callback_on_vm_init;
 
    /* JVMTI_EVENT_VM_DEATH */
    callbacks.VMDeath = &callback_on_vm_death;
 
    /* JVMTI_EVENT_CLASS_PREPARE */
    callbacks.ClassPrepare = &callback_on_class_prepare;
 
    /* JVMTI_EVENT_BREAKPOINT */
    callbacks.Breakpoint = &callback_on_breakpoint;
 
    error_code = (*jvmti)->SetEventCallbacks(jvmti, &callbacks, (jint)sizeof(callbacks));
    check_jvmti_error(jvmti, error_code, "Cannot set JVM TI callbacks");
    return error_code;
}

Callback funkce zavolaná při vstupu na řádek s nastaveným breakpointem, je velmi jednoduchá, protože pouze na standardní výstup vypíše zprávu o návštěvě breakpointu. Ovšem vzhledem k tomu, že se této callback funkci předá identifikátor vlákna, identifikátor metody s breakpointem i přesná lokace breakpointu v rámci této metody, mohly by se v této callback funkci ve skutečnosti provádět i mnohem složitější operace, protože přes parametr typu jthread lze získat celý stav vlákna, včetně obsahu zásobníkových rámců atd.:

/*
 * Callback funkce zavolana pri zapisu vybranych atributu
 */
static void JNICALL callback_on_breakpoint(
        jvmtiEnv *jvmti_env,
        JNIEnv*   jni_env,
        jthread   thread,
        jmethodID method,
        jlocation location)
{
    printf("*** visited breakpoint!!! ***\n");
}

6. Testovací třída použitá společně s demonstračním agentem číslo 31

Pro otestování korektní činnosti třicátého prvního JVM TI agenta byla vytvořena testovací třída nazvaná, jak již zajisté očekáváte, Test31. V této třídě je definováno několik metod, pro nás je však nejdůležitější metoda bar(), protože právě pro tuto metodu se nastavuje breakpoint. Ze zdrojového kódu JVM TI agenta lze získat informaci o tom, že breakpoint se nastavuje pro řádek číslo 14 – jedná se o hodnotu přiřazenou konstantě BREAKPOINT_LINE_NUMBER. V případě, že si v textovém editoru otevřete zdrojový kód třídy Test31 (soubor Test31.java), zjistíte, že tělo metody bar() se skutečně nachází na řádcích 12, 13, 14 a 15, takže k nastavení breakpointu na řádek Test31:14 skutečně může bez problému dojít:

/**
  * Testovaci trida pouzite pro test tricateho
  * prvniho demonstracniho JVM TI agenta.
  */
public class Test31 {
    public void foo() {
        System.out.println("Test31.foo()");
        bar();
    }
 
    public void bar() {
        System.out.println("Test31.bar() line 12");
        System.out.println("Test31.bar() line 13");
        System.out.println("Test31.bar() line 14");
        System.out.println("Test31.bar() line 15");
    }
 
    public void run() {
        System.out.println("Test31.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 Test31().run();
    }
 
}

7. Spuštění třicátého prvního demonstračního JVM TI agenta

Po překladu třicátého prvního demonstračního JVM TI agenta, jehož úplný zdrojový kód najdete pod tímto odkazem si můžeme vyzkoušet jeho spuštění společně s testovací třídou Test31. Spuštění se provede příkazem:

java -agentpath:./libagent31.so Test31 2> /dev/null

JVM TI agent by měl nejprve vypsat informaci o všech nalezených metodách testovací třídy Test31 a posléze by měl vypsat řetězec *** visited breakpoint!!! *** ve chvíli dosažení breakpointu. Vzhledem k tomu, že se na standardním výstupu kombinují zprávy vypisované agentem se zprávami vypisovanými testovací třídou, lze jednoduše zjistit, že byl breakpoint skutečně zaregistrován na správném místě ve zdrojovém kódu třídy Test31:

root_podpora

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
*** visited breakpoint!!! ***
Agent31: Got VM Death event
Agent31: Agent_OnUnload

Sami si můžete pro zajímavost vyzkoušet, co se stane v případě, pokud je breakpoint umístěn do programové smyčky.

8. Zdrojové kódy demonstračního agenta i k němu příslušných testovacích příkladů a skriptů

Zdrojový kód třicátého prvního demonstračního JVM TI agenta je, společně s testovací třídou Test31 i skripty použitými pro překlad a spuštění agenta, uložen do Mercurial repositáře dostupného na adrese http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/. Prozatím nejnovější verze všech zmíněných zdrojových souborů jsou dostupné na adresách:

9. 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ý?