Initialisation et fonctionnement de l'interpréteur de bytecode dans JVM HotSpot sous x86

Presque tous les développeurs Java savent que les programmes écrits en Java sont initialement compilés dans un bytecode JVM et stockés sous forme de fichiers de classe dans un format standardisé . Après avoir placé ces fichiers de classe dans la machine virtuelle et jusqu'à ce que le compilateur ne les ait pas encore atteints, la JVM interprète le bytecode contenu dans ces fichiers de classe. Cet article fournit un aperçu du fonctionnement de l'interpréteur par rapport au HotSpot OpenJDK JVM.


Le contenu de l'article:


  • L'environnement
  • Exécution d'une application Java
  • Initialisation de l'interpréteur et transfert de contrôle en code java
  • Exemple

L'environnement


Pour les expériences, nous utilisons l'assemblage de la dernière révision OpenJDK JDK12 disponible avec la configuration autoconf


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

sur Ubuntu 18.04 / gcc 7.4.0.


--with-native-debug-symbols=internal signifie que, lors de la construction du JDK, les symboles debazh seront contenus dans les binaires eux-mêmes.


--enable-debug - que le binaire contiendra du code de débogage supplémentaire.


Construire JDK 12 dans un tel environnement n'est pas un processus compliqué. Tout ce que je devais faire était d'installer JDK11 ( pour construire JDK n, JDK n-1 est requis ) et livrer manuellement les bibliothèques nécessaires signalées par autoconf. Ensuite, exécutez la commande


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

et après avoir attendu un peu (sur mon ordinateur portable environ 10 minutes), nous obtenons la construction JDK 12 de fastdebug.


En principe, il suffirait d'installer simplement jdk à partir de référentiels publics et de fournir en plus le paquet openjdk-xx-dbg avec des symboles de débogage, où xx est la version jdk, mais l'assemblage fastdebug fournit des fonctions de débogage à partir de gdb qui peuvent faciliter la vie dans certains cas. Pour le moment, j'utilise activement ps () , une fonction pour afficher les traces de pile Java à partir de gdb, et pfl () , une fonction pour analyser la pile de trames (c'est très pratique lors du débogage de l'interpréteur dans gdb).


Exemple ps () et pfl ()

Par exemple, considérez le script gdb suivant


 #   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 

Le résultat de l'exécution d'un tel script est:


 "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 

Comme vous pouvez le voir, dans le cas de ps() nous obtenons simplement la pile des appels, dans le cas de pfl() - l'organisation complète de la pile.


Exécution d'une application Java


Avant de passer directement à la discussion de l'interpréteur, nous passerons brièvement en revue les actions qui sont effectuées avant de transférer le contrôle au code java. Par exemple, prenez un programme Java qui "ne fait rien du tout":


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

et essayez de comprendre ce qui se passe lorsque vous exécutez une telle application:


javac Main.java && java Main


La première chose à faire pour répondre à cette question est de trouver et d'examiner le binaire java - celui que nous utilisons pour exécuter toutes nos applications JVM. Dans mon cas, il est situé le long du chemin


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


Mais au final, il n'y a rien de spécial à regarder. Il s'agit d'un binaire qui, avec les symboles debazhnymi, ne prend que 20 Ko et est compilé à partir d'un seul lanceur de fichier source / main.c.


Il ne fait que recevoir des arguments de ligne de commande (char * argv []), lire les arguments de la variable d'environnement JDK_JAVA_OPTIONS , effectuer un prétraitement et une validation de base (par exemple, vous ne pouvez pas ajouter d' option de terminal ou de nom de classe principale à cette variable d'environnement) et appeler la fonction JLI_Launch avec la liste d'arguments résultante.


La définition de la fonction JLI_Launch n'est pas contenue dans le binaire java et, si vous regardez ses dépendances directes:


 $ 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) 

vous pouvez voir libjli.so qui lui est lié. Cette bibliothèque contient une interface de lancement - un ensemble de fonctions que java utilise pour initialiser et démarrer une machine virtuelle, parmi lesquelles il y a JLI_Launch.


Liste complète des fonctionnalités de l'interface
 $ 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 

Après le transfert de contrôle à JLI_Launch, un certain nombre d'actions sont nécessaires pour démarrer la JVM, telles que:


I. Chargement de caractères JVM HotSpot dans la mémoire et obtention d'un pointeur sur une fonction pour créer une machine virtuelle.


Tout le code JVM HotSpot se trouve dans la bibliothèque libjvm.so. Après avoir déterminé le chemin absolu vers libjvm.so, la bibliothèque est chargée en mémoire et le pointeur vers la fonction JNI_CreateJavaVM en est arraché . Ce pointeur de fonction est stocké et ensuite utilisé pour créer et initialiser la machine virtuelle.


De toute évidence, libjvm.so n'est pas lié à libjli.so


II . Analyse des arguments passés après le prétraitement.


Une fonction avec le nom parlant ParseArguments analyse les arguments passés à partir de la ligne de commande. Cet analyseur d'arguments définit le mode de démarrage de l'application


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

Il convertit également une partie des arguments au format -DpropertyName=propertyValue , par exemple, -cp=/path converti en -Djava.class.path=/path . De plus, ces SystemProperty sont stockées dans le tableau global dans le JVM HotSpot et transmises à java.lang.System::props dans la première phase d'initialisation (dans JDK12, le mécanisme d'initialisation de java.lang.System.props a été modifié, plus dans cette validation ).


L'analyse des arguments supprime également certaines options qui ne sont pas traitées par la machine --list-modules (par exemple, --list-modules , le traitement de cette option a lieu directement dans le lanceur à ce stade ).


III . Créez un thread primordial et créez-y une machine virtuelle


Mais si quelque chose s'est mal passé, une tentative est faite pour démarrer la JVM dans le thread principal "essayez juste".


Après avoir étudié la question, j'ai trouvé l'une des raisons possibles pour lesquelles la JVM ne démarre pas dans le thread principal. Le fait est que (au moins sous Linux) pthreads et le thread principal fonctionnent différemment avec la pile. La taille du thread principal'a est limitée par ulimit -s , c'est-à-dire lors de la définition d'une valeur arbitrairement grande, nous obtenons une pile arbitrairement grande. Le thread principal utilise quelque chose de similaire à MAP_GROWSDOWN , mais pas MAP_GROWSDOWN . L'utilisation de MAP_GROWSDOWN dans sa forme pure n'est pas sûre et, si la mémoire est bonne, elle est verrouillée. Sur ma machine, MAP_GROWSDOWN n'ajoute aucun effet. La différence entre le mappage de thread principal et MAP_GROWSDOWN est qu'aucun autre mmap , à l'exception de MAP_FIXED , ne pourra créer de conflit avec la zone d'expansion possible de la pile. Tout ce qui est nécessaire à partir du logiciel est de définir la valeur rsp correspondante, puis le système d'exploitation le comprendra: Et le défaut de page sera traité et le protecteur se mettra en place . Cette différence affecte un certain nombre de râteaux: lors de la détermination de la taille de la pile du flux actuel , lors de la création de pages de garde


Ainsi, nous supposerons qu'au moment où nous avons analysé avec succès les options et créé un thread pour la machine virtuelle. Après cela, le thread juste bifurqué commence à créer une machine virtuelle et entre dans la fonction Threads :: create_vm


Dans cette fonction, un nombre assez important est fait magie noire initialisations, nous ne nous intéresserons qu'à quelques-unes d'entre elles.


Initialisation de l'interpréteur et transfert de contrôle au code java


Pour chaque instruction du JVM HotSpot, il existe un modèle de code machine spécifique pour une architecture spécifique. Lorsque l'interpréteur commence à exécuter une instruction, la première chose qu'il recherche est l'adresse de son modèle dans la table spéciale DispatchTable . Ensuite, passez à l'adresse de ce modèle et une fois l'exécution de l'instruction terminée, jvm supprime l'adresse de la prochaine instruction dans l'ordre ) et commence à l'exécuter de la même manière, etc. Ce comportement est observé avec l'interpréteur uniquement pour les instructions qui ne "distribuent" pas, par exemple, les instructions arithmétiques ( xsub , xdiv , etc., où x - i , l , f , d ). Tout ce qu'ils font, c'est effectuer des opérations arithmétiques.


Dans le cas d'instructions d'invocation de procédures ( invokestatic , invokevirtual , etc.), la prochaine instruction à exécuter sera la première instruction de la procédure appelée. De telles instructions elles-mêmes notent l'adresse de la prochaine instruction de bytecode à exécuter dans leur modèle.


Pour assurer le fonctionnement de cette machine dans Threads::create_vm , un certain nombre d'initialisations sont effectuées dont dépend l'interpréteur:


I. Initialisation d'une table des bytecodes disponibles


Avant de procéder à l'initialisation de l'interpréteur, il est nécessaire d'initialiser la table des bytecodes utilisés. Il est exécuté dans la fonction Bytecodes :: initialize et est présenté comme une étiquette très lisible. Son fragment est le suivant:


  // 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 ); 

Conformément à ce tableau, pour chaque bytecode, sa longueur est définie (la taille est toujours de 1 octet, mais il peut également y avoir un index dans ConstantPool , ainsi que des bytecodes larges), nom, bytecode et drapeaux:


 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]; 

Ces paramètres sont en outre nécessaires pour générer du code de modèle d'interpréteur.


II . Initialiser le code de cache


Afin de générer du code pour les modèles d'interpréteur, vous devez d'abord allouer de la mémoire à cette entreprise. La réservation de mémoire pour le code cache est implémentée dans une fonction du même nom CodeCache :: initialize () . Comme on peut le voir dans la section de code suivante de cette fonction


  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); } 

le code de cache est contrôlé par les options -XX:ReservedCodeCacheSize , -XX:SegmentedCodeCache , -XX:CodeCacheExpansionSize , -XX:NonNMethodCodeHeapSize , -XX:ProfiledCodeHeapSize , -XX:NonProfiledCodeHeapSize . Une brève description de ces options se trouve sur les liens auxquels elles mènent. En plus de la ligne de commande, les valeurs de certaines de ces options sont ajustées de façon ergonomique, par exemple, si la valeur SegmentedCodeCache est SegmentedCodeCache par défaut (désactivée), puis avec une taille de code >= 240Mb , SegmentedCodeCache sera inclus dans CompilerConfig :: set_tiered_flags .


Après avoir effectué les vérifications, une zone de taille octets ReservedCodeCacheSize est ReservedCodeCacheSize . Si SegmentedCodeCache s'est avéré être exposé, alors cette zone est divisée en parties: méthodes compilées JIT, routines de stabilisation, etc.


III . Initialisation des modèles d'interpréteur


Une fois la table de bytecode et le code de cache initialisés, vous pouvez procéder à la génération de code des modèles d'interpréteur. Pour ce faire, l'interpréteur réserve un tampon à partir du code de cache précédemment initialisé. À chaque étape de la génération de code, les codelets - de petites sections de code - seront coupés du tampon . Une fois la génération en cours terminée, la partie du codelet qui n'est pas utilisée par le code est libérée et devient disponible pour les générations de code suivantes.


Considérez chacune de ces étapes individuellement:



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

le gestionnaire de signature est utilisé pour préparer des arguments pour les appels aux méthodes natives. Dans ce cas, un gestionnaire générique est généré si, par exemple, la méthode native a plus de 13 arguments (je ne l'ai pas vérifié dans le débogueur, mais à en juger par le code, cela devrait être comme ça)



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

La machine virtuelle valide les fichiers de classe lors de l'initialisation, mais c'est dans le cas où les arguments sur la pile ne sont pas au format requis ou au bytecode que la machine virtuelle ne connaît pas. Ces stubs sont utilisés lors de la génération de code de modèle pour chacun des codes secondaires.



Après avoir appelé les procédures, il est nécessaire de restaurer les données de la pile de trames, qui étaient avant l'appel de la procédure à partir de laquelle le retour est effectué.



Utilisé lors de l'appel du runtime à partir d'un interprète.


  • Lancer des exceptions


  • Points d'entrée de méthode


     #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); \ } 

    Présenté sous forme de macro selon le type de méthode. Dans le cas général, la préparation de la trame de pile interprétée est effectuée, vérification StackOverflow, pile-banging. Pour les méthodes natives, un gestionnaire de signature est défini.


  • Génération de modèles de bytecode



  // 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(); 

Pour exécuter l'instruction, la spécification VM requiert que les opérandes soient dans la pile d'opérandes , mais cela n'empêche pas HotSpot de les mettre en cache dans le registre. Une énumération est utilisée pour déterminer l'état actuel du haut de la pile.


 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 }; 

Chaque instruction définit les états d'entrée et de sortie du sommet TosState de la pile, et la génération de modèles se produit en fonction de cet état. Ces modèles sont initialisés dans une table de modèles lisibles. Un fragment de ce tableau est le suivant:


 // 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 , _ ); 

Nous serons particulièrement intéressés in colonnes d' out et de generator .


in - état du haut de la pile au moment où l'instruction a commencé
out - état du haut de la pile au moment de l'achèvement
generator - générateur de modèle de code d'instruction machine


La vue générale du modèle pour tous les bytecodes peut être décrite comme suit:


  1. Si le bit de répartition n'est pas défini pour l'instruction, le prologue de l'instruction est exécuté (no-op sur x86)


  2. À l'aide du generator , le code machine est généré


  3. Si le bit de répartition n'est pas défini pour l'instruction, la transition vers l'instruction suivante dans l'ordre est effectuée en fonction de l'état de out du haut de la pile, qui sera entré pour l'instruction suivante



L'adresse du point d'entrée pour le modèle résultant est stockée dans la table globale et peut être utilisée pour le débogage.


Dans HotSpot, le code relativement stupide suivant en est responsable:


Générateur de codes d'instructions
 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 .


Exemple


, , :


 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/fr469291/


All Articles