几乎每个Java开发人员都知道用Java语言编写的程序最初会被编译为JVM字节码并存储为标准格式的类文件。 在虚拟机中获取此类类文件之后,直到编译器尚未访问它们为止,JVM会解释这些类文件中包含的字节码。 本文概述了有关OpenJDK JVM HotSpot的解释器如何工作。
文章内容:
- 环境
- 运行Java应用程序
- 解释器初始化和控制权转移到Java代码
- 例子
环境
对于实验,我们使用带有autoconf配置的最新可用 OpenJDK JDK12 修订版的汇编。
--enable-debug --with-native-debug-symbols=internal
在Ubuntu 18.04 / gcc 7.4.0上。
--with-native-debug-symbols=internal
意味着,在构建JDK时,debazh符号将包含在二进制文件本身中。
--enable-debug
二进制文件将包含其他调试代码。
在这样的环境中构建JDK 12并不复杂。 我需要做的就是安装JDK11( 要构建JDK n,需要JDK n-1 ),然后手动交付自动配置的必需库。 接下来,运行命令
bash configure --enable-debug --with-native-debug-symbols=internal && make CONF=fastdebug images
然后稍等片刻(在我的笔记本电脑上大约10分钟),我们得到了fastdebug构建JDK 12。
原则上,仅从公共存储库安装jdk并附带提供带有调试符号的openjdk-xx-dbg软件包就足够了,其中xx是jdk版本,但是fastdebug程序集提供了来自gdb的调试功能,在某些情况下可以简化工作。 目前,我正在积极使用ps()(用于查看来自gdb的Java堆栈跟踪信息的功能)和pfl()(用于分析框架堆栈的功能) (在gdb中调试解释器时非常方便)。
例子ps()和pfl()例如,考虑以下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
运行这样的脚本的结果是:
"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
如您所见,在ps()
的情况下,我们仅获得调用堆栈,在pfl()
的情况下, pfl()
完整堆栈组织。
运行Java应用程序
在直接进行解释器讨论之前,我们将简要回顾一下将控制权转移到Java代码之前执行的操作。 例如,以一个“什么都不做”的Java程序为例:
public class Main { public static void main(String args[]){ } }
并尝试找出运行此类应用程序时发生的情况:
javac Main.java && java Main
回答此问题的第一件事是查找并查看Java二进制文件-我们用于运行所有JVM应用程序的二进制文件。 就我而言,它位于路径上
/home/dmitrii/jdk12/build/linux-x86_64-server-fastdebug/images/jdk/bin/java
。
但是最后,没有什么特别值得关注的。 这是一个二进制文件,与debazhnymi符号一起仅占用20KB,并且仅从一个源文件启动器/ main.c编译。
他所做的只是接收命令行参数(char * argv []), 从JDK_JAVA_OPTIONS环境变量中读取参数 ,进行基本的预处理和验证(例如,您不能在此环境变量中添加终端选项或Main class名称)并调用该函数。 JLI_Launch及其结果参数列表。
JLI_Launch函数的定义未包含在Java二进制文件中,如果您查看其直接依赖项,则该定义:
$ 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)
您可以看到与其链接的libjli.so 。 该库包含启动器接口-Java用来初始化和启动虚拟机的一组函数,其中包括JLI_Launch。
界面功能的完整列表 $ 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
在将控制权转移到JLI_Launch之后,需要执行许多操作来启动JVM,例如:
我 将JVM HotSpot字符加载到内存中,并获得指向创建VM的函数的指针。
所有JVM HotSpot代码都位于libjvm.so库中。 在确定libjvm.so的绝对路径之后,该库将被加载到内存中 ,并且指向JNI_CreateJavaVM函数的指针也将被删除 。 该函数指针被存储,随后用于创建和初始化虚拟机。
显然libjvm.so没有链接到libjli.so
二 。 解析经过预处理后传递的参数。
具有语音名称ParseArguments的函数将解析从命令行传递的参数。 此参数解析器定义应用程序启动模式
enum LaunchMode {
还将部分参数转换为-DpropertyName=propertyValue
格式,例如, -cp=/path
转换为-Djava.class.path=/path
。 此外,此类SystemProperty
存储在JVM HotSpot的全局数组中,并在初始化的第一阶段转发给java.lang.System::props
(在JDK12中,已经修改了java.lang.System.props的初始化机制,在此commit中有更多修改)。
解析参数还会丢弃JVM未处理的一些选项(例如--list-modules
,此时此选项的处理直接在启动器中进行)。
三 。 分叉原始线程并在其中创建VM
但是,如果出现问题,则尝试在主线程中 “仅尝试一下” 启动JVM 。
研究了这个问题之后,我发现了JVM无法在主线程中启动的可能原因之一。 事实是(至少在Linux上)pthread和主线程在堆栈中的工作方式不同。 主线程'a的大小受ulimit -s
限制,即 设置任意大的值时,我们得到一个任意大的堆栈。 主线程使用类似于MAP_GROWSDOWN的东西,但不使用MAP_GROWSDOWN
。 以纯格式使用MAP_GROWSDOWN
是不安全的,并且如果内存对我MAP_GROWSDOWN
, MAP_GROWSDOWN
其锁定。 在我的机器上, MAP_GROWSDOWN
不会添加任何效果。 主线程映射和MAP_GROWSDOWN之间的区别在于,除了MAP_FIXED
之外,没有其他mmap
可以与可能的堆栈扩展区域产生冲突。 该软件所需要做的就是设置相应的rsp
值,然后操作系统将rsp
值:然后将处理页面错误,并设置保护措施 。 这种差异会影响许多耙: 在确定当前流的堆栈大小时 , 在创建保护页面时
因此,我们将假设目前我们已经成功解析了选项并为VM创建了线程。 之后,刚刚分叉的线程开始创建虚拟机,然后进入Threads :: create_vm函数
在此功能中,产生了大量 黑魔法 初始化,我们只对其中一些感兴趣。
初始化解释器并将控制权转移到Java代码
对于JVM HotSpot中的每条指令,都有用于特定体系结构的特定机器代码模板。 当解释器开始执行指令时,它首先寻找的是其模板在特殊DispatchTable表中的地址。 接下来, 跳转到该模板的地址,并在指令执行完成后,jvm 按顺序取出下一条指令的地址,并以相同的方式开始执行,依此类推。 使用解释器只能针对不“分派”的指令(例如,算术指令( xsub
, xdiv
等,其中x
- i
, l
, f
, d
))观察到此行为。 他们所做的只是执行算术运算。
对于过程调用指令( invokestatic
, invokestatic
等),要执行的下一条指令将是被调用过程中的第一条指令。 这些指令本身将下一个要在其模板中执行的字节码指令的地址放下。
为了确保本机在Threads::create_vm
,将执行许多解释程序依赖的初始化:
我 初始化可用字节码表
在进行解释器的初始化之前,有必要初始化使用的字节码表。 它在Bytecodes :: initialize函数中执行,并显示为易于阅读的标签。 其片段如下:
根据该表,为每个字节码设置其长度(大小始终为1个字节,但ConstantPool
可能还有一个索引以及宽字节码),名称,字节码和标志:
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];
进一步需要这些参数来生成解释器模板代码。
二 。 初始化缓存代码
为了生成解释器模板的代码,必须首先为此业务分配内存。 高速缓存代码的内存保留是通过具有相同名称CodeCache :: initialize()的函数实现的。 从此函数的以下代码部分可以看出
CodeCacheExpansionSize = align_up(CodeCacheExpansionSize, os::vm_page_size()); if (SegmentedCodeCache) {
缓存代码由以下选项控制-XX:ReservedCodeCacheSize
, -XX:SegmentedCodeCache
, -XX:CodeCacheExpansionSize
, -XX:NonNMethodCodeHeapSize
, -XX:ProfiledCodeHeapSize
, -XX:NonProfiledCodeHeapSize
。 这些选项的简要说明可以在它们所指向的链接中找到。 除了命令行以外,其中某些选项的值也经过了人体工程学调整,例如,如果默认情况下(关闭) SegmentedCodeCache
值(关闭),则代码大小>= 240Mb
, SegmentedCodeCache
将包含在CompilerConfig :: set_tiered_flags中 。
执行检查后,将ReservedCodeCacheSize
一个大小为ReservedCodeCacheSize
bytes的区域。 如果发现SegmentedCodeCache
是公开的,则此区域分为以下部分:JIT编译的方法,stab例程等。
三 。 解释器模式的初始化
字节码表和缓存代码初始化之后,可以继续进行解释器模板的代码生成。 为此,解释器会保留先前初始化的缓存代码中的缓冲区。 在代码生成的每个阶段,将从缓冲区中剪切出小代码(一小段代码)。 在完成当前的生成之后,小码中未被代码使用的部分将被释放,并且可用于后续代码生成。
分别考虑以下每个步骤:
{ CodeletMark cm(_masm, "slow signature handler"); AbstractInterpreter::_slow_signature_handler = generate_slow_signature_handler(); }
签名处理程序用于为调用本机方法准备参数。 在这种情况下,例如,如果本机方法具有13个以上的参数,则会生成一个通用处理程序(我没有在调试器中检查它,但是从代码中判断它应该像这样)
{ CodeletMark cm(_masm, "error exits"); _unimplemented_bytecode = generate_error_exit("unimplemented bytecode"); _illegal_bytecode_sequence = generate_error_exit("illegal bytecode sequence - method not verified"); }
VM会在初始化期间验证类文件,但这是为了防止栈上的参数不是所需格式或VM不知道的字节码。 当为每个字节码生成模板代码时,将使用这些存根。
调用过程之后,有必要从帧堆栈中还原数据,该过程是在调用返回过程的过程之前进行的。
从解释器调用运行时时使用。
抛出异常
方法入口点
#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); \ }
根据方法的类型显示为宏。 在一般情况下,将执行解释后的堆栈帧的准备工作,StackOverflow检查,堆栈爆炸 对于本机方法,定义了签名处理程序。
字节码模板生成
要执行该指令,VM规范要求将操作数放在Operand Stack中 ,但这不会阻止HotSpot将它们缓存在寄存器中。 为了确定堆栈顶部的当前状态,使用了一个枚举。
enum TosState {
每个指令定义堆栈的TosState
顶部的输入和输出状态,并且根据该状态发生模式。 这些模板在可读的模板表中初始化。 该表的一部分如下:
我们将对in
, out
和generator
列特别感兴趣。
in-指令开始时堆栈顶部的状态
out
指令完成时堆栈顶部的状态
generator
机器指令代码模板的生成器
所有字节码的模板的一般视图可以描述为:
如果未为指令设置调度位,则执行指令序言(x86上为no-op)
使用generator
机器码
如果未为该指令设置调度位,则根据堆栈顶部的出栈状态(按顺序进入下一条指令)按顺序执行到下一条指令的转换。
结果模板的入口地址存储在全局表中,可用于调试。
在HotSpot中,以下相对笨拙的代码负责:
指令码生成器 void TemplateInterpreterGenerator::set_entry_points(Bytecodes::Code code) { CodeletMark cm(_masm, Bytecodes::name(code), code);
, . JVM. Java- . JavaCalls . JVM , main .
, , :
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);
byte_no == f1_byte
— ConstantPoolCache
, , 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_invoke
— ConstantPoolCache
. , , ConstantPoolCacheEntry
__ get_cache_and_index_and_bytecode_at_bcp(Rcache, index, temp, byte_no, 1, index_size); __ cmpl(temp, code);
, 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
.