Initialisierung und Betrieb des Bytecode-Interpreters in JVM HotSpot unter x86

Fast jeder Java-Entwickler weiß, dass in Java geschriebene Programme zunächst in einen JVM-Bytecode kompiliert und als Klassendateien in einem standardisierten Format gespeichert werden . Nachdem solche Klassendateien in der virtuellen Maschine abgerufen wurden und der Compiler sie noch nicht erreicht hat, interpretiert die JVM den in diesen Klassendateien enthaltenen Bytecode. Dieser Artikel bietet einen Überblick über die Funktionsweise des Interpreters in Bezug auf den OpenJDK JVM HotSpot.


Der Inhalt des Artikels:


  • Die Umwelt
  • Java-Anwendung ausführen
  • Interpreter-Initialisierung und Steuerung der Übertragung in Java-Code
  • Beispiel

Die Umwelt


Für Experimente verwenden wir die Assembly der neuesten verfügbaren OpenJDK JDK12- Revision mit Autoconf- Konfiguration


--enable-debug --with-native-debug-symbols=internal 

unter Ubuntu 18.04 / gcc 7.4.0.


--with-native-debug-symbols=internal bedeutet, dass beim Erstellen des JDK Debazh-Symbole in den Binärdateien selbst enthalten sind.


--enable-debug - dass die Binärdatei zusätzlichen Debugging-Code enthält.


Das Erstellen von JDK 12 in einer solchen Umgebung ist kein komplizierter Prozess. Alles, was ich tun musste, war, JDK11 zu installieren ( JDK n-1 ist erforderlich, um JDK n zu erstellen ) und die erforderlichen Bibliotheken per Hand zu liefern, für die eine automatische Konfiguration signalisiert wurde. Führen Sie als Nächstes den Befehl aus


 bash configure --enable-debug --with-native-debug-symbols=internal && make CONF=fastdebug images 

und nachdem wir ein bisschen gewartet haben (auf meinem Laptop ungefähr 10 Minuten), bekommen wir fastdebug Build JDK 12.


Im Prinzip würde es völlig ausreichen, jdk einfach aus öffentlichen Repositorys zu installieren und zusätzlich das Paket openjdk-xx-dbg mit Debug-Symbolen bereitzustellen, wobei xx die jdk-Version ist. Die Fastdebug-Assembly bietet jedoch Debugging-Funktionen von gdb, die in einigen Fällen das Leben erleichtern können. Im Moment verwende ich aktiv ps () , eine Funktion zum Anzeigen von Java-Stack-Traces von gdb, und pfl () , eine Funktion zum Analysieren des Frame- Stacks (sehr praktisch beim Debuggen des Interpreters in gdb).


Beispiel ps () und pfl ()

Betrachten Sie beispielsweise das folgende GDB-Skript


 #   java file /home/dmitrii/jdk12/build/linux-x86_64-server-fastdebug/images/jdk/bin/java #   SEGV-, HotSpot #  SEGV  . #, https://hg.openjdk.java.net/jdk/jdk12/file/06222165c35f/src/hotspot/cpu/x86/vm_version_x86.cpp#l361 handle SIGSEGV nostop noprint set breakpoint pending on set pagination off #  ,   #    # java- public static void main(String args[]) b PostJVMInit thread 2 commands #   , #    set $buf = (char *) malloc(1000) #        #(   ) b *AbstractInterpreter::_entry_table[0] thread 2 commands #      rbx. #   Method* set $mthd = ((Method *) $rbx) #    $buf call $mthd->name_and_sig_as_C_string($buf, 1000) # ,  public static void main(String args) if strcmp()("Main.main([Ljava/lang/String;)V", $buf) == 0 #   ,      # ps/pfl        #(    ps/pfl) b InterpreterRuntime::build_method_counters(JavaThread*, Method*) commands #  ,    #   delete breakpoints call ps() call pfl() c end end c end c end r -cp /home/dmitrii/jdk12/ Main 

Das Ergebnis der Ausführung eines solchen Skripts ist:


 "Executing ps" for thread: "main" #1 prio=5 os_prio=0 cpu=468,61ms elapsed=58,65s tid=0x00007ffff001b800 nid=0x5bfa runnable [0x00007ffff7fd9000] java.lang.Thread.State: RUNNABLE Thread: 0x00007ffff001b800 [0x5bfa] State: _running _has_called_back 0 _at_poll_safepoint 0 JavaThread state: _thread_in_Java 1 - frame( sp=0x00007ffff7fd9920, unextended_sp=0x00007ffff7fd9920, fp=0x00007ffff7fd9968, pc=0x00007fffd828748b) Main.main(Main.java:10) "Executing pfl" for thread: "main" #1 prio=5 os_prio=0 cpu=468,83ms elapsed=58,71s tid=0x00007ffff001b800 nid=0x5bfa runnable [0x00007ffff7fd9000] java.lang.Thread.State: RUNNABLE Thread: 0x00007ffff001b800 [0x5bfa] State: _running _has_called_back 0 _at_poll_safepoint 0 JavaThread state: _thread_in_Java [Describe stack layout] 0x00007ffff7fd99e0: 0x00007ffff7fd9b00 #2 entry frame call_stub word fp - 0 0x00007ffff7fd99d8: 0x00007ffff7fd9c10 call_stub word fp - 1 0x00007ffff7fd99d0: 0x00007fffd8287160 call_stub word fp - 2 0x00007ffff7fd99c8: 0x00007fffbf1fb3e0 call_stub word fp - 3 0x00007ffff7fd99c0: 0x000000000000000a call_stub word fp - 4 0x00007ffff7fd99b8: 0x00007ffff7fd9ce8 call_stub word fp - 5 0x00007ffff7fd99b0: 0x00007ffff7fd9a80 call_stub word fp - 6 0x00007ffff7fd99a8: 0x00007ffff001b800 call_stub word fp - 7 0x00007ffff7fd99a0: 0x00007ffff7fd9b40 call_stub word fp - 8 0x00007ffff7fd9998: 0x00007ffff7fd9c00 call_stub word fp - 9 0x00007ffff7fd9990: 0x00007ffff7fd9a80 call_stub word fp - 10 0x00007ffff7fd9988: 0x00007ffff7fd9ce0 call_stub word fp - 11 0x00007ffff7fd9980: 0x00007fff00001fa0 call_stub word fp - 12 0x00007ffff7fd9978: 0x0000000716a122b8 sp for #2 locals for #1 unextended_sp for #2 local 0 0x00007ffff7fd9970: 0x00007fffd82719f3 0x00007ffff7fd9968: 0x00007ffff7fd99e0 #1 method Main.main([Ljava/lang/String;)V @ 0 - 1 locals 1 max stack 0x00007ffff7fd9960: 0x00007ffff7fd9978 interpreter_frame_sender_sp 0x00007ffff7fd9958: 0x0000000000000000 interpreter_frame_last_sp 0x00007ffff7fd9950: 0x00007fffbf1fb3e0 interpreter_frame_method 0x00007ffff7fd9948: 0x0000000716a11c40 interpreter_frame_mirror 0x00007ffff7fd9940: 0x0000000000000000 interpreter_frame_mdp 0x00007ffff7fd9938: 0x00007fffbf1fb5e8 interpreter_frame_cache 0x00007ffff7fd9930: 0x00007ffff7fd9978 interpreter_frame_locals 0x00007ffff7fd9928: 0x00007fffbf1fb3d0 interpreter_frame_bcp 0x00007ffff7fd9920: 0x00007ffff7fd9920 sp for #1 interpreter_frame_initial_sp unextended_sp for #1 

Wie Sie sehen können, erhalten wir im Fall von ps() nur den Aufrufstapel, im Fall von pfl() die vollständige pfl() .


Java-Anwendung ausführen


Bevor wir direkt mit der Diskussion des Interpreters fortfahren, werden wir kurz die Aktionen überprüfen, die ausgeführt werden, bevor die Steuerung auf den Java-Code übertragen wird. Nehmen Sie zum Beispiel ein Java-Programm, das "überhaupt nichts tut":


 public class Main { public static void main(String args[]){ } } 

und versuchen Sie herauszufinden, was passiert, wenn Sie eine solche Anwendung ausführen:


javac Main.java && java Main


Um diese Frage zu beantworten, müssen Sie zunächst die Java-Binärdatei suchen und anzeigen, mit der wir alle unsere JVM-Anwendungen ausführen. In meinem Fall befindet es sich entlang des Pfades


/home/dmitrii/jdk12/build/linux-x86_64-server-fastdebug/images/jdk/bin/java .


Aber am Ende gibt es nichts Besonderes zu sehen. Dies ist eine Binärdatei, die zusammen mit Debazhnymi-Symbolen nur 20 KB benötigt und aus nur einer Quelldatei launcher / main.c. kompiliert wird.


Er empfängt lediglich Befehlszeilenargumente (char * argv []), liest die Argumente aus der Umgebungsvariablen JDK_JAVA_OPTIONS , führt eine grundlegende Vorverarbeitung und Validierung durch (z. B. können Sie dieser Umgebungsvariablen keine Terminaloption oder keinen Hauptklassennamen hinzufügen) und die Funktion aufrufen JLI_Launch mit der resultierenden Argumentliste.


Die Definition der JLI_Launch-Funktion ist nicht in der Java-Binärdatei enthalten, und wenn Sie sich die direkten Abhängigkeiten ansehen:


 $ ldd java linux-vdso.so.1 (0x00007ffcc97ec000) libjli.so => /home/dmitrii/jdk12/build/linux-x86_64-server-fastdebug/images/jdk/bin/./../lib/libjli.so (0x00007ff27518d000) // <---------    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff274d9c000) libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007ff274b7f000) libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007ff27497b000) libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007ff27475c000) /lib64/ld-linux-x86-64.so.2 (0x00007ff27559f000) 

Sie können libjli.so sehen, das damit verknüpft ist. Diese Bibliothek enthält die Launcher-Oberfläche - eine Reihe von Funktionen, mit denen Java eine virtuelle Maschine initialisiert und startet, darunter JLI_Launch.


Vollständige Liste der Schnittstellenfunktionen
 $ objdump -T -j .text libjli.so libjli.so: file format elf64-x86-64 DYNAMIC SYMBOL TABLE: 0000000000009280 g DF .text 0000000000000038 Base JLI_List_add 0000000000003330 g DF .text 00000000000001c3 Base JLI_PreprocessArg 0000000000008180 g DF .text 0000000000000008 Base JLI_GetStdArgs 0000000000008190 g DF .text 0000000000000008 Base JLI_GetStdArgc 0000000000007e50 g DF .text 00000000000000b8 Base JLI_ReportErrorMessage 000000000000a400 g DF .text 00000000000000df Base JLI_ManifestIterate 0000000000002e70 g DF .text 0000000000000049 Base JLI_InitArgProcessing 0000000000008000 g DF .text 0000000000000011 Base JLI_ReportExceptionDescription 0000000000003500 g DF .text 0000000000000074 Base JLI_AddArgsFromEnvVar 0000000000007f10 g DF .text 00000000000000e9 Base JLI_ReportErrorMessageSys 0000000000005840 g DF .text 00000000000000b8 Base JLI_ReportMessage 0000000000009140 g DF .text 000000000000003a Base JLI_SetTraceLauncher 0000000000009020 g DF .text 000000000000000a Base JLI_MemFree 0000000000008f90 g DF .text 0000000000000026 Base JLI_MemAlloc 00000000000059c0 g DF .text 0000000000002013 Base JLI_Launch 00000000000091c0 g DF .text 000000000000003b Base JLI_List_new 0000000000008ff0 g DF .text 0000000000000026 Base JLI_StringDup 0000000000002ec0 g DF .text 000000000000000c Base JLI_GetAppArgIndex 

Nach der Übertragung der Steuerung an JLI_Launch sind eine Reihe von Aktionen erforderlich, um die JVM zu starten, z.


I. Laden von JVM-HotSpot-Zeichen in den Speicher und Abrufen eines Zeigers auf eine Funktion zum Erstellen einer VM.


Der gesamte JVM-HotSpot-Code befindet sich in der Bibliothek libjvm.so. Nachdem der absolute Pfad zu libjvm.so ermittelt wurde, wird die Bibliothek in den Speicher geladen und der Zeiger auf die Funktion JNI_CreateJavaVM wird herausgerissen . Dieser Funktionszeiger wird gespeichert und anschließend zum Erstellen und Initialisieren der virtuellen Maschine verwendet.


Offensichtlich ist libjvm.so nicht mit libjli.so verknüpft


II . Parsing-Argumente, die nach der Vorverarbeitung übergeben wurden.


Eine Funktion mit dem sprechenden Namen ParseArguments analysiert die über die Befehlszeile übergebenen Argumente. Dieser Argumentparser definiert den Anwendungsstartmodus


 enum LaunchMode { // cf. sun.launcher.LauncherHelper LM_UNKNOWN = 0, LM_CLASS, LM_JAR, LM_MODULE, LM_SOURCE }; 

Außerdem wird ein Teil der Argumente in das Format -DpropertyName=propertyValue konvertiert. Beispielsweise wird -cp=/path in -Djava.class.path=/path konvertiert. Darüber hinaus werden solche SystemProperty im globalen Array im JVM-HotSpot gespeichert und in der ersten Phase der Initialisierung an java.lang.System::props weitergeleitet (In JDK12 wurde der Initialisierungsmechanismus von java.lang.System.props geändert, mehr in diesem Commit ).


Beim Parsen von Argumenten werden auch einige Optionen --list-modules , die nicht von der JVM verarbeitet werden (z. B. --list-modules , die Verarbeitung dieser Option erfolgt zu diesem Zeitpunkt direkt im Launcher).


III . Gabeln Sie einen ursprünglichen Thread und erstellen Sie eine VM darin


Aber wenn etwas schief gelaufen ist, wird versucht, die JVM im Hauptthread zu starten. "Probieren Sie es einfach aus."


Nachdem ich die Frage untersucht hatte, fand ich einen der möglichen Gründe, warum die JVM nicht im Hauptthread startet. Tatsache ist, dass (zumindest unter Linux) Pthreads und der Hauptthread mit dem Stack unterschiedlich funktionieren. Die Größe des Hauptfadens ist durch ulimit -s -s begrenzt, d.h. Wenn Sie einen beliebig großen Wert festlegen, erhalten Sie einen beliebig großen Stapel. Der Hauptthread verwendet etwas Ähnliches wie MAP_GROWSDOWN , jedoch nicht MAP_GROWSDOWN . Die Verwendung von MAP_GROWSDOWN in seiner reinen Form ist nicht sicher und, wenn der Speicher mir recht MAP_GROWSDOWN , gesperrt. Auf meinem Computer MAP_GROWSDOWN keinen Effekt hinzu. Der Unterschied zwischen der Haupt-Thread-Zuordnung und MAP_GROWSDOWN besteht darin, dass keine andere mmap mit Ausnahme von MAP_FIXED einen Konflikt mit dem Bereich einer möglichen MAP_FIXED kann. Alles, was von der Software benötigt wird, ist, den entsprechenden rsp Wert einzustellen, und dann wird das Betriebssystem es herausfinden: Und der Seitenfehler wird verarbeitet und der Schutz wird eingestellt . Dieser Unterschied betrifft eine Reihe von Rechen: Beim Bestimmen der Stapelgröße des aktuellen Streams , beim Erstellen von Schutzseiten


Wir gehen also davon aus, dass wir im Moment die Optionen erfolgreich analysiert und einen Thread für die VM erstellt haben. Danach beginnt der gerade gegabelte Thread mit der Erstellung einer virtuellen Maschine und ruft die Funktion Threads :: create_vm auf


In dieser Funktion wird eine ziemlich große Anzahl gemacht schwarze Magie Initialisierungen werden uns nur an wenigen interessieren.


Initialisierung des Interpreters und Übertragung der Kontrolle auf den Java-Code


Für jede Anweisung im JVM-HotSpot gibt es eine bestimmte Maschinencodevorlage für eine bestimmte Architektur. Wenn der Interpreter mit der Ausführung einer Anweisung beginnt, sucht er zunächst nach der Adresse seiner Vorlage in der speziellen DispatchTable- Tabelle. Springen Sie als Nächstes zur Adresse dieser Vorlage. Nachdem die Ausführung der Anweisung abgeschlossen ist, nimmt jvm die Adresse der nächsten Anweisung in der angegebenen Reihenfolge heraus und beginnt, sie auf die gleiche Weise auszuführen, und so weiter. Dieses Verhalten wird mit dem Interpreter nur für Anweisungen beobachtet, die nicht "versenden", z. B. arithmetische Anweisungen ( xsub , xdiv usw., wobei x - i , l , f , d ). Sie führen lediglich arithmetische Operationen aus.


Im Fall von Prozeduraufrufanweisungen ( invokestatic , invokevirtual usw.) ist die nächste invokevirtual Anweisung die erste Anweisung in der aufgerufenen Prozedur. Solche Anweisungen selbst geben die Adresse der nächsten Bytecode-Anweisung an, die in ihrer Vorlage ausgeführt werden soll.


Um den Betrieb dieses Computers in Threads::create_vm , werden eine Reihe von Initialisierungen durchgeführt, von denen der Interpreter abhängt:


I. Initialisieren einer Tabelle verfügbarer Bytecodes


Bevor Sie mit der Initialisierung des Interpreters fortfahren, müssen Sie die Tabelle der verwendeten Bytecodes initialisieren. Es wird in der Funktion Bytecodes :: initialize ausgeführt und als gut lesbare Bezeichnung dargestellt. Sein Fragment ist wie folgt:


  // Java bytecodes // bytecode bytecode name format wide f. result tp stk traps def(_nop , "nop" , "b" , NULL , T_VOID , 0, false); def(_aconst_null , "aconst_null" , "b" , NULL , T_OBJECT , 1, false); def(_iconst_m1 , "iconst_m1" , "b" , NULL , T_INT , 1, false); def(_iconst_0 , "iconst_0" , "b" , NULL , T_INT , 1, false); def(_iconst_1 , "iconst_1" , "b" , NULL , T_INT , 1, false); def(_iconst_2 , "iconst_2" , "b" , NULL , T_INT , 1, false); def(_iconst_3 , "iconst_3" , "b" , NULL , T_INT , 1, false); def(_iconst_4 , "iconst_4" , "b" , NULL , T_INT , 1, false); def(_iconst_5 , "iconst_5" , "b" , NULL , T_INT , 1, false); def(_lconst_0 , "lconst_0" , "b" , NULL , T_LONG , 2, false); def(_lconst_1 , "lconst_1" , "b" , NULL , T_LONG , 2, false); def(_fconst_0 , "fconst_0" , "b" , NULL , T_FLOAT , 1, false); def(_fconst_1 , "fconst_1" , "b" , NULL , T_FLOAT , 1, false); def(_fconst_2 , "fconst_2" , "b" , NULL , T_FLOAT , 1, false); def(_dconst_0 , "dconst_0" , "b" , NULL , T_DOUBLE , 2, false); def(_dconst_1 , "dconst_1" , "b" , NULL , T_DOUBLE , 2, false); def(_bipush , "bipush" , "bc" , NULL , T_INT , 1, false); def(_sipush , "sipush" , "bcc" , NULL , T_INT , 1, false); def(_ldc , "ldc" , "bk" , NULL , T_ILLEGAL, 1, true ); def(_ldc_w , "ldc_w" , "bkk" , NULL , T_ILLEGAL, 1, true ); def(_ldc2_w , "ldc2_w" , "bkk" , NULL , T_ILLEGAL, 2, true ); 

In Übereinstimmung mit dieser Tabelle wird für jeden Bytecode seine Länge festgelegt (die Größe beträgt immer 1 Byte, es kann jedoch auch einen Index in ConstantPool , sowie breite Bytecodes), Name, Bytecode und Flags:


 bool Bytecodes::_is_initialized = false; const char* Bytecodes::_name [Bytecodes::number_of_codes]; BasicType Bytecodes::_result_type [Bytecodes::number_of_codes]; s_char Bytecodes::_depth [Bytecodes::number_of_codes]; u_char Bytecodes::_lengths [Bytecodes::number_of_codes]; Bytecodes::Code Bytecodes::_java_code [Bytecodes::number_of_codes]; unsigned short Bytecodes::_flags [(1<<BitsPerByte)*2]; 

Diese Parameter werden ferner benötigt, um Interpreter-Vorlagencode zu generieren.


II . Cache-Code initialisieren


Um Code für Interpreter-Vorlagen zu generieren, müssen Sie zuerst Speicher für dieses Unternehmen zuweisen. Die Speicherreservierung für Cache-Code ist in einer gleichnamigen Funktion CodeCache :: initialize () implementiert. Wie aus dem folgenden Codeabschnitt dieser Funktion ersichtlich ist


  CodeCacheExpansionSize = align_up(CodeCacheExpansionSize, os::vm_page_size()); if (SegmentedCodeCache) { // Use multiple code heaps initialize_heaps(); } else { // Use a single code heap FLAG_SET_ERGO(uintx, NonNMethodCodeHeapSize, 0); FLAG_SET_ERGO(uintx, ProfiledCodeHeapSize, 0); FLAG_SET_ERGO(uintx, NonProfiledCodeHeapSize, 0); ReservedCodeSpace rs = reserve_heap_memory(ReservedCodeCacheSize); add_heap(rs, "CodeCache", CodeBlobType::All); } 

Der Cache-Code wird durch die Optionen gesteuert - -XX:ReservedCodeCacheSize , -XX:SegmentedCodeCache , -XX:CodeCacheExpansionSize , -XX:NonNMethodCodeHeapSize , -XX:ProfiledCodeHeapSize -XX:NonProfiledCodeHeapSize , -XX:NonProfiledCodeHeapSize . Eine kurze Beschreibung dieser Optionen finden Sie unter den Links, zu denen sie führen. Zusätzlich zur Befehlszeile werden die Werte einiger dieser Optionen ergonomisch angepasst. Wenn beispielsweise der SegmentedCodeCache Wert standardmäßig SegmentedCodeCache wird (aus), wird SegmentedCodeCache mit einer Codegröße >= 240Mb in CompilerConfig :: set_tiered_flags enthalten sein .


Nach dem Durchführen von Überprüfungen wird ein Bereich mit der Größe ReservedCodeCacheSize Bytes ReservedCodeCacheSize . Wenn sich herausstellt, dass SegmentedCodeCache , ist dieser Bereich in Teile unterteilt: JIT-kompilierte Methoden, Stichroutinen usw.


III . Initialisierung von Interpreter-Mustern


Nachdem die Bytecode-Tabelle und der Cache-Code initialisiert wurden, können Sie mit der Codegenerierung der Interpreter-Vorlagen fortfahren. Zu diesem Zweck reserviert der Interpreter einen Puffer aus dem zuvor initialisierten Cache-Code. In jeder Phase der Codegenerierung werden Codelets - kleine Codeabschnitte - aus dem Puffer herausgeschnitten . Nach Abschluss der aktuellen Generation wird der Teil des Codelets, der nicht vom Code verwendet wird, freigegeben und steht für nachfolgende Codegenerierungen zur Verfügung.


Betrachten Sie jeden dieser Schritte einzeln:



  { CodeletMark cm(_masm, "slow signature handler"); AbstractInterpreter::_slow_signature_handler = generate_slow_signature_handler(); } 

Der Signaturhandler wird verwendet, um Argumente für Aufrufe nativer Methoden vorzubereiten. In diesem Fall wird ein generischer Handler generiert, wenn die native Methode beispielsweise mehr als 13 Argumente enthält (ich habe sie im Debugger nicht überprüft, aber nach dem Code sollte sie so aussehen).



  { CodeletMark cm(_masm, "error exits"); _unimplemented_bytecode = generate_error_exit("unimplemented bytecode"); _illegal_bytecode_sequence = generate_error_exit("illegal bytecode sequence - method not verified"); } 

Die VM überprüft Klassendateien während der Initialisierung. Dies ist jedoch der Fall, wenn die Argumente auf dem Stapel nicht das erforderliche Format oder den Bytecode haben, den die VM nicht kennt. Diese Stubs werden beim Generieren von Vorlagencode für jeden der Bytecodes verwendet.



Nach dem Aufruf der Prozeduren müssen die Daten aus dem Frame-Stack wiederhergestellt werden, der vor dem Aufruf der Prozedur vorhanden war, von der die Rückgabe erfolgt.



Wird verwendet, wenn die Laufzeit von einem Interpreter aufgerufen wird.


  • Ausnahmen werfen


  • Methodeneintrittspunkte


     #define method_entry(kind) \ { CodeletMark cm(_masm, "method entry point (kind = " #kind ")"); \ Interpreter::_entry_table[Interpreter::kind] = generate_method_entry(Interpreter::kind); \ Interpreter::update_cds_entry_table(Interpreter::kind); \ } 

    Wird je nach Art der Methode als Makro dargestellt. Im allgemeinen Fall wird die Vorbereitung des interpretierten Stapelrahmens durchgeführt, StackOverflow-Prüfung, Stack-Banging. Für native Methoden wird ein Signaturhandler definiert.


  • Generierung von Bytecode-Vorlagen



  // Bytecodes set_entry_points_for_all_bytes(); // installation of code in other places in the runtime // (ExcutableCodeManager calls not needed to copy the entries) set_safepoints_for_all_bytes(); 

Um die Anweisung auszuführen, müssen sich Operanden in der VM-Spezifikation im Operandenstapel befinden. Dies hindert HotSpot jedoch nicht daran, sie im Register zwischenzuspeichern. Eine Aufzählung wird verwendet, um den aktuellen Status der Oberseite des Stapels zu bestimmen.


 enum TosState { // describes the tos cache contents btos = 0, // byte, bool tos cached ztos = 1, // byte, bool tos cached ctos = 2, // char tos cached stos = 3, // short tos cached itos = 4, // int tos cached ltos = 5, // long tos cached ftos = 6, // float tos cached dtos = 7, // double tos cached atos = 8, // object cached vtos = 9, // tos not cached number_of_states, ilgl // illegal state: should not occur }; 

Jeder Befehl definiert die Eingabe- und Ausgabezustände der TosState Oberseite des Stapels, und die Erzeugung von Mustern erfolgt abhängig von diesem Zustand. Diese Vorlagen werden in einer lesbaren Vorlagentabelle initialisiert. Ein Fragment dieser Tabelle lautet wie folgt:


 // interpr. templates // Java spec bytecodes ubcp|disp|clvm|iswd in out generator argument def(Bytecodes::_nop , ____|____|____|____, vtos, vtos, nop , _ ); def(Bytecodes::_aconst_null , ____|____|____|____, vtos, atos, aconst_null , _ ); def(Bytecodes::_iconst_m1 , ____|____|____|____, vtos, itos, iconst , -1 ); def(Bytecodes::_iconst_0 , ____|____|____|____, vtos, itos, iconst , 0 ); def(Bytecodes::_iconst_1 , ____|____|____|____, vtos, itos, iconst , 1 ); def(Bytecodes::_iconst_2 , ____|____|____|____, vtos, itos, iconst , 2 ); def(Bytecodes::_iconst_3 , ____|____|____|____, vtos, itos, iconst , 3 ); def(Bytecodes::_iconst_4 , ____|____|____|____, vtos, itos, iconst , 4 ); def(Bytecodes::_iconst_5 , ____|____|____|____, vtos, itos, iconst , 5 ); def(Bytecodes::_lconst_0 , ____|____|____|____, vtos, ltos, lconst , 0 ); def(Bytecodes::_lconst_1 , ____|____|____|____, vtos, ltos, lconst , 1 ); def(Bytecodes::_fconst_0 , ____|____|____|____, vtos, ftos, fconst , 0 ); def(Bytecodes::_fconst_1 , ____|____|____|____, vtos, ftos, fconst , 1 ); def(Bytecodes::_fconst_2 , ____|____|____|____, vtos, ftos, fconst , 2 ); def(Bytecodes::_dconst_0 , ____|____|____|____, vtos, dtos, dconst , 0 ); def(Bytecodes::_dconst_1 , ____|____|____|____, vtos, dtos, dconst , 1 ); def(Bytecodes::_bipush , ubcp|____|____|____, vtos, itos, bipush , _ ); def(Bytecodes::_sipush , ubcp|____|____|____, vtos, itos, sipush , _ ); 

Wir werden uns besonders für in , out und generator Spalten interessieren.


in - der Status der Oberseite des Stapels zum Zeitpunkt des Beginns der Anweisung
out - Zustand der Oberseite des Stapels zum Zeitpunkt des Abschlusses der Anweisung
generator - Generator der Maschinenanweisungscode-Vorlage


Die allgemeine Ansicht der Vorlage für alle Bytecodes kann wie folgt beschrieben werden:


  1. Wenn für den Befehl kein Versandbit gesetzt ist, wird der Befehlsprolog ausgeführt (No-Op auf x86).


  2. Mit dem generator wird Maschinencode generiert


  3. Wenn das Versandbit für den Befehl nicht gesetzt ist, wird der Übergang zum nächsten Befehl in der Reihenfolge ausgeführt, abhängig vom out der Oberseite des Stapels, der für den nächsten Befehl eingeht



Die Einstiegspunktadresse für die resultierende Vorlage wird in der globalen Tabelle gespeichert und kann zum Debuggen verwendet werden.


In HotSpot ist der folgende relativ dumme Code dafür verantwortlich:


Anweisungscode-Generator
 void TemplateInterpreterGenerator::set_entry_points(Bytecodes::Code code) { CodeletMark cm(_masm, Bytecodes::name(code), code); // initialize entry points assert(_unimplemented_bytecode != NULL, "should have been generated before"); assert(_illegal_bytecode_sequence != NULL, "should have been generated before"); address bep = _illegal_bytecode_sequence; address zep = _illegal_bytecode_sequence; address cep = _illegal_bytecode_sequence; address sep = _illegal_bytecode_sequence; address aep = _illegal_bytecode_sequence; address iep = _illegal_bytecode_sequence; address lep = _illegal_bytecode_sequence; address fep = _illegal_bytecode_sequence; address dep = _illegal_bytecode_sequence; address vep = _unimplemented_bytecode; address wep = _unimplemented_bytecode; // code for short & wide version of bytecode if (Bytecodes::is_defined(code)) { Template* t = TemplateTable::template_for(code); assert(t->is_valid(), "just checking"); set_short_entry_points(t, bep, cep, sep, aep, iep, lep, fep, dep, vep); } if (Bytecodes::wide_is_defined(code)) { Template* t = TemplateTable::template_for_wide(code); assert(t->is_valid(), "just checking"); set_wide_entry_point(t, wep); } // set entry points EntryPoint entry(bep, zep, cep, sep, aep, iep, lep, fep, dep, vep); Interpreter::_normal_table.set_entry(code, entry); Interpreter::_wentry_point[code] = wep; } //... void TemplateInterpreterGenerator::set_short_entry_points(Template* t, address& bep, address& cep, address& sep, address& aep, address& iep, address& lep, address& fep, address& dep, address& vep) { assert(t->is_valid(), "template must exist"); switch (t->tos_in()) { case btos: case ztos: case ctos: case stos: ShouldNotReachHere(); // btos/ctos/stos should use itos. break; case atos: vep = __ pc(); __ pop(atos); aep = __ pc(); generate_and_dispatch(t); break; case itos: vep = __ pc(); __ pop(itos); iep = __ pc(); generate_and_dispatch(t); break; case ltos: vep = __ pc(); __ pop(ltos); lep = __ pc(); generate_and_dispatch(t); break; case ftos: vep = __ pc(); __ pop(ftos); fep = __ pc(); generate_and_dispatch(t); break; case dtos: vep = __ pc(); __ pop(dtos); dep = __ pc(); generate_and_dispatch(t); break; case vtos: set_vtos_entry_points(t, bep, cep, sep, aep, iep, lep, fep, dep, vep); break; default : ShouldNotReachHere(); break; } } //... void TemplateInterpreterGenerator::generate_and_dispatch(Template* t, TosState tos_out) { if (PrintBytecodeHistogram) histogram_bytecode(t); #ifndef PRODUCT // debugging code if (CountBytecodes || TraceBytecodes || StopInterpreterAt > 0) count_bytecode(); if (PrintBytecodePairHistogram) histogram_bytecode_pair(t); if (TraceBytecodes) trace_bytecode(t); if (StopInterpreterAt > 0) stop_interpreter_at(); __ verify_FPU(1, t->tos_in()); #endif // !PRODUCT int step = 0; if (!t->does_dispatch()) { step = t->is_wide() ? Bytecodes::wide_length_for(t->bytecode()) : Bytecodes::length_for(t->bytecode()); if (tos_out == ilgl) tos_out = t->tos_out(); // compute bytecode size assert(step > 0, "just checkin'"); // setup stuff for dispatching next bytecode if (ProfileInterpreter && VerifyDataPointer && MethodData::bytecode_has_profile(t->bytecode())) { __ verify_method_data_pointer(); } __ dispatch_prolog(tos_out, step); } // generate template t->generate(_masm); // advance if (t->does_dispatch()) { #ifdef ASSERT // make sure execution doesn't go beyond this point if code is broken __ should_not_reach_here(); #endif // ASSERT } else { // dispatch to next bytecode __ dispatch_epilog(tos_out, step); } } 

, . JVM. Java- . JavaCalls . JVM , main .


Beispiel


, , :


 public class Sum{ public static void sum(int a, int b){ return a + b; } } public class Main { public static void main(String args[]){ Sum.sum(2, 3); } } 

Sum.sum(II) .


2 javac -c *.java , .
Sum.sum :


  descriptor: (II)I flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=2 0: iload_0 1: iload_1 2: iadd 3: ireturn LineNumberTable: line 3: 0 

Main.main


  descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: iconst_2 1: iconst_3 2: invokestatic #2 // Method Sum.sum:(II)I 5: pop 6: return LineNumberTable: line 13: 0 line 14: 6 

, — .


invokestatic ' x86 - HotSpot


 void TemplateTable::invokestatic(int byte_no) { transition(vtos, vtos); assert(byte_no == f1_byte, "use this argument"); prepare_invoke(byte_no, rbx); // get f1 Method* // do the call __ profile_call(rax); __ profile_arguments_type(rax, rbx, rbcp, false); __ jump_from_interpreted(rbx, rax); } 

byte_no == f1_byteConstantPoolCache , , rbx — , Method * . : , , ( method_entry ).


prepare_invoke . , invokestatic ConstantPool Constant_Methodref_Info . HotSpot . 2 .. ConstantPoolCache . ConstantPoolCache , (, ConstantPoolCacheEntry , ). ConstantPoolCacheEntry , ( 0) / . , ConstantPool , ConstantPoolCache ( x86 Little Endian).


, , HotSpot prepare_invokeConstantPoolCache . , , ConstantPoolCacheEntry


  __ get_cache_and_index_and_bytecode_at_bcp(Rcache, index, temp, byte_no, 1, index_size); __ cmpl(temp, code); // have we resolved this bytecode? __ jcc(Assembler::equal, resolved); // resolve first time through address entry = CAST_FROM_FN_PTR(address, InterpreterRuntime::resolve_from_cache); __ movl(temp, code); __ call_VM(noreg, entry, temp); // Update registers with resolved info __ get_cache_and_index_at_bcp(Rcache, index, 1, index_size); __ bind(resolved); 

, InterpreterRuntime::resolve_from_cache .


receiver'a , . (, , , ConstantPoolCache <clinit> , ). define class, EagerInitialization ( , , :)). HotSpot ( CDS ) .


, , ConstantPoolCacheEntry . Method * rbx , , .


Sum.sum(2, 3) . gdb-script sum.gdb :


 #    java file /home/dmitrii/jdk12/build/linux-x86_64-server-fastdebug/images/jdk/bin/java # gdb    SEGV' #,   https://hg.openjdk.java.net/jdk/jdk12/file/06222165c35f/src/hotspot/cpu/x86/vm_version_x86.cpp#l361 handle SIGSEGV nostop noprint #       set breakpoint pending on #    , #    set pagination off #      main b PostJVMInit commands #   , #    set $buffer = malloc(1000) #   . #jmp       # invokestatic b *AbstractInterpreter::_entry_table[0] thread 2 commands #     invokestatic, # Method*   rbx set $mthd = (Method *) $rbx #    $buffer call $mthd->name_and_sig_as_C_string($buffer, 1000) if strcmp()($buffer, "Sum.sum(II)I") == 0 #  iload_0,     b *TemplateInterpreter::_normal_table._table[vtos][26] thread 2 #  iload_1,   - int,  #  iload_0 b *TemplateInterpreter::_normal_table._table[itos][27] thread 2 #   iadd b *TemplateInterpreter::_normal_table._table[itos][96] thread 2 end c end c end r -cp . Main 

gdb -x sum.gdb , Sum.sum


 $453 = 0x7ffff7fdcdd0 "Sum.sum(II)I" 

layout asm , , generate_normal_entry . -, StackOverflow, stack-banging dispatch iload_0 . :


 0x7fffd828fa1f mov eax,DWORD PTR [r14] ;, iload_0 0x7fffd828fa22 movzx ebx,BYTE PTR [r13+0x1] ;   0x7fffd828fa27 inc r13 ; bcp (byte code pointer) 0x7fffd828fa2a movabs r10,0x7ffff717e8a0 ; DispatchTable 0x7fffd828fa34 jmp QWORD PTR [r10+rbx*8] ;jump      

rax ,


 0x7fffd828fabe push rax ;     ;   ,      0x7fffd828fabf mov eax,DWORD PTR [r14-0x8] 0x7fffd828fac3 movzx ebx,BYTE PTR [r13+0x1] 0x7fffd828fac8 inc r13 0x7fffd828facb movabs r10,0x7ffff717e8a0 0x7fffd828fad5 jmp QWORD PTR [r10+rbx*8] 

iadd :


 0x7fffd8292ba7 mov edx,DWORD PTR [rsp] ; ,     iload_1 0x7fffd8292baa add rsp,0x8 ; rsp    0x7fffd8292bae add eax,edx ;   0x7fffd8292bb0 movzx ebx,BYTE PTR [r13+0x1] 0x7fffd8292bb5 inc r13 0x7fffd8292bb8 movabs r10,0x7ffff717e8a0 0x7fffd8292bc2 jmp QWORD PTR [r10+rbx*8] 

gdb eax edx ,


 (gdb) p $eax $457 = 3 (gdb) p $edx $458 = 2 

, Sum.sum .

Source: https://habr.com/ru/post/de469291/


All Articles