Inicialização e operação do interpretador de bytecode no JVM HotSpot sob x86

Quase todo desenvolvedor Java sabe que os programas escritos em Java são inicialmente compilados em um bytecode da JVM e armazenados como arquivos de classe em um formato padronizado . Depois de colocar esses arquivos de classe dentro da máquina virtual e até que o compilador ainda os alcance, a JVM interpreta o bytecode contido nesses arquivos de classe. Este artigo fornece uma visão geral de como o intérprete trabalha em relação ao OpenJDK JVM HotSpot.


O conteúdo do artigo:


  • O meio ambiente
  • Executando aplicativo java
  • Inicialização de intérpretes e transferência de controle para código java
  • Exemplo

O meio ambiente


Para experimentos, usamos a montagem da última revisão disponível do OpenJDK JDK12 com configuração autoconf


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

no Ubuntu 18.04 / gcc 7.4.0.


--with-native-debug-symbols=internal significa que, ao criar o JDK, os símbolos debazh estarão contidos nos próprios binários.


--enable-debug - que o binário conterá código de depuração adicional.


Construir o JDK 12 em um ambiente como esse não é um processo complicado. Tudo o que eu precisava era instalar o JDK11 ( para construir o JDK n, é necessário o JDK n-1 ) e entregar em mão as bibliotecas necessárias que o autoconf sinalizou. Em seguida, execute o comando


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

e depois de esperar um pouco (no meu laptop cerca de 10 minutos), obtemos o fastdebug build JDK 12.


Em princípio, seria suficiente simplesmente instalar o jdk a partir de repositórios públicos e entregar adicionalmente o pacote openjdk-xx-dbg com símbolos de depuração, onde xx é a versão do jdk, mas o assembly fastdebug fornece funções de depuração do gdb que podem facilitar a vida em alguns casos. No momento, estou usando ativamente ps () , uma função para visualizar rastreamentos de pilha Java do gdb, e pfl () , uma função para analisar a pilha de quadros (é muito conveniente ao depurar o interpretador no gdb).


Exemplo ps () e pfl ()

Por exemplo, considere o seguinte script gdb


 #   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 

O resultado da execução desse script é:


 "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 

Como você pode ver, no caso de ps() obtemos apenas a pilha de chamadas, no caso de pfl() - a organização da pilha completa.


Executando aplicativo java


Antes de prosseguir diretamente para a discussão do intérprete, revisaremos brevemente as ações executadas antes de transferir o controle para o código java. Por exemplo, considere um programa Java que "não faz nada":


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

e tente descobrir o que acontece quando você executa esse aplicativo:


javac Main.java && java Main


A primeira coisa a fazer para responder a essa pergunta é encontrar e examinar o binário java - aquele que usamos para executar todos os nossos aplicativos JVM. No meu caso, ele está localizado ao longo do caminho


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


Mas no final, não há nada de especial para assistir. Este é um binário que, junto com os símbolos debazhnymi, leva apenas 20 KB e é compilado a partir de apenas um iniciador de arquivos de origem / main.c.


Tudo o que ele faz é receber argumentos de linha de comando (char * argv []), ler os argumentos da variável de ambiente JDK_JAVA_OPTIONS, executar pré-processamento e validação básicos (por exemplo, você não pode adicionar uma opção de terminal ou nome da classe principal a essa variável de ambiente) e chamar a função JLI_Launch com a lista de argumentos resultante.


A definição da função JLI_Launch não está contida no binário java e, se você observar suas dependências diretas:


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

você pode ver o libjli.so que está vinculado a ele. Esta biblioteca contém a interface do iniciador - um conjunto de funções que o java usa para inicializar e iniciar uma máquina virtual, entre as quais o JLI_Launch.


Lista completa de recursos da 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 

Após a transferência do controle para JLI_Launch, são necessárias várias ações para iniciar a JVM, como:


Eu Carregando caracteres JVM HotSpot na memória e obtendo um ponteiro para uma função para criar uma VM.


Todo o código do JVM HotSpot está localizado na biblioteca libjvm.so. Após determinar o caminho absoluto para libjvm.so, a biblioteca é carregada na memória e o ponteiro para a função JNI_CreateJavaVM é arrancado dela . Esse ponteiro de função é armazenado e subsequentemente usado para criar e inicializar a máquina virtual.


Obviamente libjvm.so não está vinculado ao libjli.so


II Os argumentos de análise transmitidos após o pré-processamento.


Uma função com o nome falante ParseArguments analisa os argumentos transmitidos da linha de comando. Este analisador de argumentos define o modo de inicialização do aplicativo


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

Ele também converte parte dos argumentos no formato -DpropertyName=propertyValue , por exemplo, -cp=/path convertido em -Djava.class.path=/path . Além disso, esse SystemProperty é armazenado na matriz global no JVM HotSpot e encaminhado para java.lang.System::props na primeira fase da inicialização (no JDK12, o mecanismo de inicialização do java.lang.System.props foi modificado, mais neste commit ).


Os argumentos de análise também descartam algumas opções que não são processadas pela JVM (por exemplo --list-modules , o processamento dessa opção ocorre diretamente no iniciador neste momento ).


III Bifurque um encadeamento primordial e crie uma VM nele


Mas se algo der errado, é feita uma tentativa de iniciar a JVM no encadeamento principal "apenas tente".


Tendo estudado a questão, encontrei uma das possíveis razões pelas quais a JVM não inicia no encadeamento principal. O fato é que (pelo menos no Linux) pthreads e o thread principal funcionam de maneira diferente com a pilha. O tamanho do thread-principal é limitado por ulimit -s , ou seja, ao definir um valor arbitrariamente grande, obtemos uma pilha arbitrariamente grande. O thread principal usa algo semelhante ao MAP_GROWSDOWN , mas não MAP_GROWSDOWN . Usar MAP_GROWSDOWN em sua forma pura não é seguro e, se a memória me servir corretamente, está bloqueado. Na minha máquina, o MAP_GROWSDOWN não adiciona nenhum efeito. A diferença entre o mapeamento do thread principal e o MAP_GROWSDOWN é que nenhum outro mmap , com exceção de MAP_FIXED , poderá entrar em conflito com a área de possível expansão de pilha. Tudo o que é necessário no software é definir o valor rsp correspondente e o sistema operacional descobrirá: E a falha de página será processada e a proteção será definida . Essa diferença afeta um número de rakes: Ao determinar o tamanho da pilha do fluxo atual , ao criar páginas de proteção


Portanto, assumiremos que no momento analisamos com êxito as opções e criamos um encadeamento para a VM. Depois disso, o encadeamento bifurcado começa a criar uma máquina virtual e entra na função Threads :: create_vm


Nesta função, um número bastante grande é feito magia negra inicializações, estaremos interessados ​​em apenas algumas delas.


Inicialização do intérprete e transferência de controle para o código java


Para cada instrução no JVM HotSpot, existe um modelo de código de máquina específico para uma arquitetura específica. Quando o intérprete começa a executar uma instrução, a primeira coisa que procura é o endereço do seu modelo na tabela especial DispatchTable . Em seguida, pule para o endereço desse modelo e após a execução da instrução ser concluída, o jvm pega o endereço da próxima instrução em ordem ) e começa a executá-lo da mesma maneira e assim por diante. Esse comportamento é observado com o intérprete apenas para instruções que não "despacham", por exemplo, instruções aritméticas ( xsub , xdiv , etc, onde x - i , l , f , d ). Tudo o que eles fazem é executar operações aritméticas.


No caso de instruções de chamada de procedimentos ( invokevirtual , invokevirtual , etc.), a próxima instrução a ser executada será a primeira instrução no procedimento chamado. Essas instruções colocam o endereço da próxima instrução bytecode a ser executada em seu modelo.


Para garantir a operação desta máquina em Threads::create_vm , são executadas várias inicializações das quais o intérprete depende:


Eu Inicializando uma tabela de bytecodes disponíveis


Antes de prosseguir com a inicialização do intérprete, é necessário inicializar a tabela de bytecodes usados. É executado na função Bytecodes :: initialize e é apresentado como um rótulo muito legível. Seu fragmento é o seguinte:


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

De acordo com esta tabela, para cada bytecode, seu comprimento é definido (o tamanho é sempre 1 byte, mas também pode haver um índice no ConstantPool , além de largos bytecodes), nome, bytecode e sinalizadores:


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

Esses parâmetros ainda são necessários para gerar o código do modelo do intérprete.


II Inicializar código de cache


Para gerar código para modelos de intérprete, você deve alocar memória para esse negócio. A reserva de memória para o código de cache é implementada em uma função com o mesmo nome CodeCache :: initialize () . Como pode ser visto na seção de código a seguir desta função


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

o código do cache é controlado pelas opções -XX:ReservedCodeCacheSize , -XX:SegmentedCodeCache , -XX:CodeCacheExpansionSize , -XX:NonNMethodCodeHeapSize , -XX:ProfiledCodeHeapSize , -XX:NonProfiledCodeHeapSize . Uma breve descrição dessas opções pode ser encontrada nos links para os quais eles levam. Além da linha de comando, os valores de algumas dessas opções são ajustados ergonomicamente, por exemplo, se o valor SegmentedCodeCache for SegmentedCodeCache por padrão (desativado), com um tamanho de código >= 240Mb , o SegmentedCodeCache será incluído em CompilerConfig :: set_tiered_flags .


Após executar as verificações, uma área de tamanho ReservedCodeCacheSize bytes é ReservedCodeCacheSize . Se SegmentedCodeCache for exposto, essa área será dividida em partes: métodos compilados por JIT, rotinas de facada, etc.


III Inicialização de padrões de intérpretes


Depois que a tabela de bytecode e o código do cache forem inicializados, você poderá prosseguir para a geração do código dos modelos do interpretador. Para fazer isso, o intérprete reserva um buffer do código de cache inicializado anteriormente. Em cada estágio da geração do código, as codelets - pequenas seções de código - serão cortadas do buffer . Após a conclusão da geração atual, a parte do codelet que não é usada pelo código é liberada e fica disponível para as gerações subsequentes de código.


Considere cada uma destas etapas individualmente:



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

O manipulador de assinaturas é usado para preparar argumentos para chamadas para métodos nativos. Nesse caso, um manipulador genérico é gerado se, por exemplo, o método nativo tiver mais de 13 argumentos (não o verifiquei no depurador, mas, a julgar pelo código, deve ser assim)



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

A VM valida os arquivos de classe durante a inicialização, mas isso ocorre caso os argumentos na pilha não estejam no formato necessário ou no código de bytes que a VM não conhece. Esses stubs são usados ​​ao gerar código de modelo para cada um dos bytecodes.



Após chamar os procedimentos, é necessário restaurar os dados da pilha de quadros, que era anterior ao procedimento a partir do qual o retorno é feito.



Usado ao chamar o tempo de execução de um intérprete.


  • Lançando exceções


  • Pontos de entrada do método


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

    Apresentado como uma macro, dependendo do tipo de método. No caso geral, é realizada a preparação do quadro de pilha interpretado, verificação StackOverflow, bate de pilha. Para métodos nativos, um manipulador de assinaturas é definido.


  • Geração de modelo 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(); 

Para executar a instrução, a especificação da VM exige que os operandos estejam na Pilha de Operandos , mas isso não impede que o HotSpot os armazene em cache no registro. Para determinar o estado atual da parte superior da pilha, uma enumeração é usada .


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

Cada instrução define os estados de entrada e saída da parte superior da pilha do TosState e a geração de padrões ocorre dependendo desse estado. Esses modelos são inicializados em uma tabela de modelos legível. Um fragmento desta tabela é o seguinte:


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

Estaremos especialmente interessados in colunas de out e generator .


in - o estado do topo da pilha no momento em que a instrução começou
estado de out do topo da pilha no momento da conclusão da instrução
generator - modelo de código de instrução de máquina


A visão geral do modelo para todos os bytecodes pode ser descrita como:


  1. Se o bit de despacho não estiver definido para a instrução, o prólogo da instrução será executado (no-op no x86)


  2. Usando generator , o código da máquina é gerado


  3. Se o bit de despacho não estiver definido para a instrução, a transição para a próxima instrução em ordem será executada dependendo do estado de out da parte superior da pilha, que estará presente in próxima instrução



O endereço do ponto de entrada para o modelo resultante é armazenado na tabela global e pode ser usado para depuração.


No HotSpot, o seguinte código relativamente idiota é responsável por isso:


Gerador de código de instruções
 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 .


Exemplo


, , :


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


All Articles