【JVM源码解析】模板解释器解释执行Java字节码指令(上)
本文由HeapDump性能社区首席讲师鸠摩(马智)授权整理发布
第22篇-虚拟机字节码之运算指令
虚拟机规范中与运算相关的字节码指令如下表所示。
0x60 | iadd | 将栈顶两int型数值相加并将结果压入栈顶 |
0x61 | ladd | 将栈顶两long型数值相加并将结果压入栈顶 |
0x62 | fadd | 将栈顶两float型数值相加并将结果压入栈顶 |
0x63 | dadd | 将栈顶两double型数值相加并将结果压入栈顶 |
0x64 | isub | 将栈顶两int型数值相减并将结果压入栈顶 |
0x65 | lsub | 将栈顶两long型数值相减并将结果压入栈顶 |
0x66 | fsub | 将栈顶两float型数值相减并将结果压入栈顶 |
0x67 | dsub | 将栈顶两double型数值相减并将结果压入栈顶 |
0x68 | imul | 将栈顶两int型数值相乘并将结果压入栈顶 |
0x69 | lmul | 将栈顶两long型数值相乘并将结果压入栈顶 |
0x6a | fmul | 将栈顶两float型数值相乘并将结果压入栈顶 |
0x6b | dmul | 将栈顶两double型数值相乘并将结果压入栈顶 |
0x6c | idiv | 将栈顶两int型数值相除并将结果压入栈顶 |
0x6d | ldiv | 将栈顶两long型数值相除并将结果压入栈顶 |
0x6e | fdiv | 将栈顶两float型数值相除并将结果压入栈顶 |
0x6f | ddiv | 将栈顶两double型数值相除并将结果压入栈顶 |
0x70 | irem | 将栈顶两int型数值作取模运算并将结果压入栈顶 |
0x71 | lrem | 将栈顶两long型数值作取模运算并将结果压入栈顶 |
0x72 | frem | 将栈顶两float型数值作取模运算并将结果压入栈顶 |
0x73 | drem | 将栈顶两double型数值作取模运算并将结果压入栈顶 |
0x74 | ineg | 将栈顶int型数值取负并将结果压入栈顶 |
0x75 | lneg | 将栈顶long型数值取负并将结果压入栈顶 |
0x76 | fneg | 将栈顶float型数值取负并将结果压入栈顶 |
0x77 | dneg | 将栈顶double型数值取负并将结果压入栈顶 |
0x78 | ishl | 将int型数值左移位指定位数并将结果压入栈顶 |
0x79 | lshl | 将long型数值左移位指定位数并将结果压入栈顶 |
0x7a | ishr | 将int型数值右(符号)移位指定位数并将结果压入栈顶 |
0x7b | lshr | 将long型数值右(符号)移位指定位数并将结果压入栈顶 |
0x7c | iushr | 将int型数值右(无符号)移位指定位数并将结果压入栈顶 |
0x7d | lushr | 将long型数值右(无符号)移位指定位数并将结果压入栈顶 |
0x7e | iand | 将栈顶两int型数值作“按位与”并将结果压入栈顶 |
0x7f | land | 将栈顶两long型数值作“按位与”并将结果压入栈顶 |
0x80 | ior | 将栈顶两int型数值作“按位或”并将结果压入栈顶 |
0x81 | lor | 将栈顶两long型数值作“按位或”并将结果压入栈顶 |
0x82 | ixor | 将栈顶两int型数值作“按位异或”并将结果压入栈顶 |
0x83 | lxor | 将栈顶两long型数值作“按位异或”并将结果压入栈顶 |
0x84 | iinc | 将指定int型变量增加指定值(i++、i--、i+=2) |
0x94 | lcmp | 比较栈顶两long型数值大小,并将结果(1、0或-1)压入栈顶 |
0x95 | fcmpl | 比较栈顶两float型数值大小,并将结果(1、0或-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶 |
0x96 | fcmpg | 比较栈顶两float型数值大小,并将结果(1、0或-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶 |
0x97 | dcmpl | 比较栈顶两double型数值大小,并将结果(1、0或-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶 |
0x98 | dcmpg | 比较栈顶两double型数值大小,并将结果(1、0或-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶 |
1、基本加、减、乘与除指令
1、iadd指令
iadd指令将两个栈顶的整数相加,然后将相加的结果压入栈顶,其指令的格式如下:
iadd val1,val2
val1与val2表示两个int类型的整数,在指令执行时,将val1与val3从操作数栈中出栈,将这两个数值相加得到 int 类型数据 result,将result压入操作数栈中。
iadd指令的模板定义如下:
def(Bytecodes::_iadd , ____|____|____|____, itos, itos, iop2 , add);
生成函数为TemplateTable::iop2(),实现如下:
void TemplateTable::iop2(Operation op) { switch (op) { case add : __ pop_i(rdx); __ addl (rax, rdx); break; case sub : __ movl(rdx, rax); __ pop_i(rax); __ subl (rax, rdx); break; case mul : __ pop_i(rdx); __ imull(rax, rdx); break; case _and : __ pop_i(rdx); __ andl (rax, rdx); break; case _or : __ pop_i(rdx); __ orl (rax, rdx); break; case _xor : __ pop_i(rdx); __ xorl (rax, rdx); break; case shl : __ movl(rcx, rax); __ pop_i(rax); __ shll (rax); break; case shr : __ movl(rcx, rax); __ pop_i(rax); __ sarl (rax); break; case ushr : __ movl(rcx, rax); __ pop_i(rax); __ shrl (rax); break; default : ShouldNotReachHere(); } }
可以看到,这个函数是许多指令的生成函数,如iadd、isub、imul、iand、ior、ixor、ishl、ishr、iushr。
为iadd指令生成的汇编代码如下:
mov (%rsp),%edx add $0x8,%rsp add %edx,%eax
将栈顶与栈顶中缓存的%eax相加后的结果存储到%eax中。
2、isub指令
isub指令生成的汇编代码如下:
mov %eax,%edx mov (%rsp),%eax add $0x8,%rsp sub %edx,%eax
代码实现比较简单,这里不再介绍。
3、idiv指令
idiv是字节码除法指令,这个指令的格式如下:
idiv val1,val2
val1 和 val2 都必须为 int 类型数据,指令执行时,val1 和 val2从操作数栈中出栈,并且将这两个数值相除(val1÷val2),结果转换为 int 类型值 result,最后 result 被压入到操作数栈中。
idiv指令的模板定义如下:
def(Bytecodes::_idiv , ____|____|____|____, itos, itos, idiv , _ );
调用的生成函数为TemplateTable::idiv(),生成的汇编如下:
0x00007fffe1019707: mov %eax,%ecx 0x00007fffe1019709: mov (%rsp),%eax 0x00007fffe101970c: add $0x8,%rsp // 测试一下被除数是否为0x80000000,如果不是,就跳转到normal_case 0x00007fffe1019710: cmp $0x80000000,%eax 0x00007fffe1019716: jne 0x00007fffe1019727 // 被除数是0x80000000,而除数如果是-1的话,则跳转到special_case 0x00007fffe101971c: xor %edx,%edx 0x00007fffe101971e: cmp $0xffffffff,%ecx 0x00007fffe1019721: je 0x00007fffe101972a // -- normal_case -- // cltd将eax寄存器中的数据符号扩展到edx:eax,具体就是 // 把eax的32位整数扩展为64位,高32位用eax的符号位填充保存到edx 0x00007fffe1019727: cltd 0x00007fffe1019728: idiv %ecx // -- special_case --
其中idiv函数会使用规定的寄存器,如下图所示。
汇编对0x80000000 / -1 这个特殊的除法做了检查。参考:利用反汇编调试与补码解释0x80000000 / -1整形输出异常不一致
2、比较指令
lcmp指令比较栈顶两long型数值大小,并将结果(1、0或-1)压入栈顶。指令的格式如下:
lcmp val1,val2
val1 和 val2 都必须为 long 类型数据,指令执行时, val1 和 val2从操作数栈中出栈,使用一个 int 数值作为比较结果:
- 如果 val1 大于val2,结果为 1;
- 如果 val1 等于 val2,结果为 0;
- 如果 val1小于 val2,结果为-1。
最后比较结果被压入到操作数栈中。
lcmp字节码指令的模板定义如下:
def(Bytecodes::_lcmp , ____|____|____|____, ltos, itos, lcmp , _ );
生成函数为TemplateTable::lcmp(), 生成的汇编如下:
0x00007fffe101a6c8: mov (%rsp),%rdx 0x00007fffe101a6cc: add $0x10,%rsp // cmp指令描述如下: // 第1操作数<第2操作数时,ZF=0 // 第1操作数=第2操作数时,ZF=1 // 第1操作数>第2操作数时,ZF=0 0x00007fffe101a6d0: cmp %rax,%rdx 0x00007fffe101a6d3: mov $0xffffffff,%eax // 将-1移到%eax中 // 如果第1操作数小于第2操作数就跳转到done 0x00007fffe101a6d8: jl 0x00007fffe101a6e0 // cmp指令执行后,执行setne指令就能获取比较的结果 // 根据eflags中的状态标志(CF,SF,OF,ZF和PF)将目标操作数设置为0或1 0x00007fffe101a6da: setne %al 0x00007fffe101a6dd: movzbl %al,%eax // -- done --
如上汇编代码的逻辑非常简单,这里不再介绍。
关于其它字节码指令的逻辑也相对简单,有兴趣的可自行研究。
第23篇-虚拟机字节码指令之类型转换
Java虚拟机规范中定义的类型转换相关的字节码指令如下表所示。
0x85 | i2l | 将栈顶int型数值强制转换成long型数值并将结果压入栈顶 |
0x86 | i2f | 将栈顶int型数值强制转换成float型数值并将结果压入栈顶 |
0x87 | i2d | 将栈顶int型数值强制转换成double型数值并将结果压入栈顶 |
0x88 | l2i | 将栈顶long型数值强制转换成int型数值并将结果压入栈顶 |
0x89 | l2f | 将栈顶long型数值强制转换成float型数值并将结果压入栈顶 |
0x8a | l2d | 将栈顶long型数值强制转换成double型数值并将结果压入栈顶 |
0x8b | f2i | 将栈顶float型数值强制转换成int型数值并将结果压入栈顶 |
0x8c | f2l | 将栈顶float型数值强制转换成long型数值并将结果压入栈顶 |
0x8d | f2d | 将栈顶float型数值强制转换成double型数值并将结果压入栈顶 |
0x8e | d2i | 将栈顶double型数值强制转换成int型数值并将结果压入栈顶 |
0x8f | d2l | 将栈顶double型数值强制转换成long型数值并将结果压入栈顶 |
0x90 | d2f | 将栈顶double型数值强制转换成float型数值并将结果压入栈顶 |
0x91 | i2b | 将栈顶int型数值强制转换成byte型数值并将结果压入栈顶 |
0x92 | i2c | 将栈顶int型数值强制转换成char型数值并将结果压入栈顶 |
0x93 | i2s | 将栈顶int型数值强制转换成short型数值并将结果压入栈顶 |
上表字节码指令的模板定义如下:
def(Bytecodes::_i2l , ____|____|____|____, itos, ltos, convert , _ ); def(Bytecodes::_i2f , ____|____|____|____, itos, ftos, convert , _ ); def(Bytecodes::_i2d , ____|____|____|____, itos, dtos, convert , _ ); def(Bytecodes::_l2i , ____|____|____|____, ltos, itos, convert , _ ); def(Bytecodes::_l2f , ____|____|____|____, ltos, ftos, convert , _ ); def(Bytecodes::_l2d , ____|____|____|____, ltos, dtos, convert , _ ); def(Bytecodes::_f2i , ____|____|____|____, ftos, itos, convert , _ ); def(Bytecodes::_f2l , ____|____|____|____, ftos, ltos, convert , _ ); def(Bytecodes::_f2d , ____|____|____|____, ftos, dtos, convert , _ ); def(Bytecodes::_d2i , ____|____|____|____, dtos, itos, convert , _ ); def(Bytecodes::_d2l , ____|____|____|____, dtos, ltos, convert , _ ); def(Bytecodes::_d2f , ____|____|____|____, dtos, ftos, convert , _ ); def(Bytecodes::_i2b , ____|____|____|____, itos, itos, convert , _ ); def(Bytecodes::_i2c , ____|____|____|____, itos, itos, convert , _ ); def(Bytecodes::_i2s , ____|____|____|____, itos, itos, convert , _ );
相关字节码转换指令的生成函数为TemplateTable::convert(),此函数的实现如下:
void TemplateTable::convert() { static const int64_t is_nan = 0x8000000000000000L; // Conversion switch (bytecode()) { case Bytecodes::_i2l: __ movslq(rax, rax); break; case Bytecodes::_i2f: __ cvtsi2ssl(xmm0, rax); break; case Bytecodes::_i2d: __ cvtsi2sdl(xmm0, rax); break; case Bytecodes::_i2b: __ movsbl(rax, rax); break; case Bytecodes::_i2c: __ movzwl(rax, rax); break; case Bytecodes::_i2s: __ movswl(rax, rax); break; case Bytecodes::_l2i: __ movl(rax, rax); break; case Bytecodes::_l2f: __ cvtsi2ssq(xmm0, rax); break; case Bytecodes::_l2d: __ cvtsi2sdq(xmm0, rax); break; case Bytecodes::_f2i: { Label L; __ cvttss2sil(rax, xmm0); __ cmpl(rax, 0x80000000); // NaN or overflow/underflow? __ jcc(Assembler::notEqual, L); __ call_VM_leaf(CAST_FROM_FN_PTR(address, SharedRuntime::f2i), 1); __ bind(L); } break; case Bytecodes::_f2l: { Label L; __ cvttss2siq(rax, xmm0); // NaN or overflow/underflow? __ cmp64(rax, ExternalAddress((address) &is_nan)); __ jcc(Assembler::notEqual, L); __ call_VM_leaf(CAST_FROM_FN_PTR(address, SharedRuntime::f2l), 1); __ bind(L); } break; case Bytecodes::_f2d: __ cvtss2sd(xmm0, xmm0); break; case Bytecodes::_d2i: { Label L; __ cvttsd2sil(rax, xmm0); __ cmpl(rax, 0x80000000); // NaN or overflow/underflow? __ jcc(Assembler::notEqual, L); __ call_VM_leaf(CAST_FROM_FN_PTR(address, SharedRuntime::d2i), 1); __ bind(L); } break; case Bytecodes::_d2l: { Label L; __ cvttsd2siq(rax, xmm0); // NaN or overflow/underflow? __ cmp64(rax, ExternalAddress((address) &is_nan)); __ jcc(Assembler::notEqual, L); __ call_VM_leaf(CAST_FROM_FN_PTR(address, SharedRuntime::d2l), 1); __ bind(L); } break; case Bytecodes::_d2f: __ cvtsd2ss(xmm0, xmm0); break; default: ShouldNotReachHere(); } }
如_i2l指令将栈顶int型数值强制转换成long型数值并将结果压入栈顶,其对应的汇编代码如下:
movslq %eax,%rax // 将一个双字扩展后送到一个四字中
对于浮点数float或long转int或long类型相对复杂,下面看一个float转int类型的f2i指令。
// 把标量单精度数转换为占用双字的标量整数 0x00007fffe1019189: vcvttss2si %xmm0,%eax // 和0x80000000进行比较,如果不相等,则跳转到L 0x00007fffe101918d: cmp $0x80000000,%eax 0x00007fffe1019193: jne 0x00007fffe10191bc // 如果栈顶指针已经按16字节对齐,则可直接调用调用SharedRuntime::f2i()函数,否则 // 将栈顶指令按16字节对齐后再调用 0x00007fffe1019199: test $0xf,%esp 0x00007fffe101919f: je 0x00007fffe10191b7 0x00007fffe10191a5: sub $0x8,%rsp // 调用SharedRuntime::f2i()函数 0x00007fffe10191a9: callq 0x00007ffff6a0f946 0x00007fffe10191ae: add $0x8,%rsp 0x00007fffe10191b2: jmpq 0x00007fffe10191bc // 调用SharedRuntime::f2i()函数 0x00007fffe10191b7: callq 0x00007ffff6a0f946 ---- L ----
生成的汇编指令vcvttss2si的意思为把标量单精度数转换为占用双字的标量整数,名称的由来解读如下:
cvt:convert,转换;
t:truncation,截断;
ss:scalar single,标量单精度数;
2:to;
si:scalar integer,标量整数。
调用的SharedRuntime::f2i()函数的实现如下:
JRT_LEAF(jint, SharedRuntime::f2i(jfloat x)) if (g_isnan(x)) // 如果为非数字值,直接返回0 return 0; if (x >= (jfloat) max_jint) return max_jint; if (x <= (jfloat) min_jint) return min_jint; return (jint) x; JRT_END
C++函数调用时,需要一个参数x,在GNU / Linux上遵循System V AMD64 ABI的调用约定。寄存器RDI,RSI,RDX,RCX,R8和R9是用于整数和存储器地址的参数和XMM0,XMM1,XMM2,XMM3,XMM4,XMM5,XMM6和XMM7用于浮点参数,所以将用xmm0做为第1个参数,这个参数恰好是栈顶用来缓存浮点数的寄存器,所以默认不用任何操作。
返回值存储到%rax中,由于tos_out为itos,%rax寄存器用来做栈顶缓存,所以也不需要做额外的操作。
第24篇-虚拟机对象操作指令之getstatic
Java虚拟机规范中定义的对象操作相关的字节码指令如下表所示。
0xb2 | getstatic | 获取指定类的静态域,并将其值压入栈顶 |
0xb3 | putstatic | 为指定的类的静态域赋值 |
0xb4 | getfield | 获取指定类的实例域,并将其值压入栈顶 |
0xb5 | putfield | 为指定的类的实例域赋值 |
0xbb | new | 创建一个对象,并将其引用值压入栈顶 |
0xbc | newarray | 创建一个指定原始类型(如int,、float,、char等)的数组,并将其引用值压入栈顶 |
0xbd | anewarray | 创建一个引用型(如类、接口或数组)的数组,并将其引用值压入栈顶 |
0xbe | arraylength | 获得数组的长度值并压入栈顶 |
0xc0 | checkcast | 检验类型转换,检验未通过将抛出ClassCastException |
0xc1 | instanceof | 检验对象是否是指定的类的实例,如果是将1压入栈顶,否则将0压入栈顶 |
0xc5 | multianewarray | 创建指定类型和指定维度的多维数组(执行该指令时,操作栈中必须包含各维度的长度值),并将其引用值压入栈顶 |
字节码指令的模板定义如下:
def(Bytecodes::_getstatic , ubcp|____|clvm|____, vtos, vtos, getstatic , f1_byte ); def(Bytecodes::_putstatic , ubcp|____|clvm|____, vtos, vtos, putstatic , f2_byte ); def(Bytecodes::_getfield , ubcp|____|clvm|____, vtos, vtos, getfield , f1_byte ); def(Bytecodes::_putfield , ubcp|____|clvm|____, vtos, vtos, putfield , f2_byte ); def(Bytecodes::_new , ubcp|____|clvm|____, vtos, atos, _new , _ ); def(Bytecodes::_newarray , ubcp|____|clvm|____, itos, atos, newarray , _ ); def(Bytecodes::_anewarray , ubcp|____|clvm|____, itos, atos, anewarray , _ ); def(Bytecodes::_multianewarray , ubcp|____|clvm|____, vtos, atos, multianewarray , _ ); def(Bytecodes::_arraylength , ____|____|____|____, atos, itos, arraylength , _ ); def(Bytecodes::_checkcast , ubcp|____|clvm|____, atos, atos, checkcast , _ ); def(Bytecodes::_instanceof , ubcp|____|clvm|____, atos, itos, instanceof , _ );
new字节码指令的生成函数为TemplateTable::_new(),这在《深入剖析Java虚拟机:源码剖析与实例详解(基础卷)》的第9章类对象创建时详细介绍过,这里不再介绍。
getstatic字节码指令获取指定类的静态域,并将其值压入栈顶。格式如下:
getstatic indexbyte1 indexbyte2
无符号数indexbyte1和indexbyte2构建为(indexbyte1<<8)|indexbyte2,这个值指明了一个当前类的运行时常量池索引值,指向的运行时常量池项为一个字段的符号引用。
getstatic字节码指令的生成函数为TemplateTable::getstatic(),还有个类似的getfield指令,这些生成函数如下:
void TemplateTable::getfield(int byte_no) { getfield_or_static(byte_no, false); // getfield的byte_no值为1 } void TemplateTable::getstatic(int byte_no) { getfield_or_static(byte_no, true); // getstatic的byte_no的值为1 }
最终都会调用getfield_or_static()函数生成机器指令片段。此函数生成的机器指令片段对应的汇编代码如下:
// 获取ConstantPoolCache中ConstantPoolCacheEntry的index 0x00007fffe101fd10: movzwl 0x1(%r13),%edx // 从栈中获取ConstantPoolCache的首地址 0x00007fffe101fd15: mov -0x28(%rbp),%rcx // 左移2位,因为%edx中存储的是ConstantPoolCacheEntry index, // 左移2位是因为ConstantPoolCacheEntry的内存占用是4个字 0x00007fffe101fd19: shl $0x2,%edx // 计算%rcx+%rdx*8+0x10,获取ConstantPoolCacheEntry[_indices,_f1,_f2,_flags]中的_indices // 因为ConstantPoolCache的大小为0x16字节,%rcx+0x10定位到第一个ConstantPoolCacheEntry的开始位置 // %rdx*8算出来的是相对于第一个ConstantPoolCacheEntry的字节偏移 0x00007fffe101fd1c: mov 0x10(%rcx,%rdx,8),%ebx // _indices向右移动16位后获取[get bytecode,set bytecode,original constant pool index]中的get bytecode与set bytecode 0x00007fffe101fd20: shr $0x10,%ebx // 获取set bytecode字段的值 0x00007fffe101fd23: and $0xff,%ebx // 0xb2是getstatic指令的Opcode,比较值,如果相等就说明已经连接,跳转到resolved 0x00007fffe101fd29: cmp $0xb2,%ebx 0x00007fffe101fd2f: je 0x00007fffe101fdce // 将getstatic字节码的Opcode存储到%ebx中 0x00007fffe101fd35: mov $0xb2,%ebx // 省略通过调用MacroAssembler::call_VM()函数来执行InterpreterRuntime::resolve_get_put()函数的汇编代码 // ...
调用MacroAssembler::call_VM()函数生成如下代码,通过这些代码来执行InterpreterRuntime::resolve_get_put()函数。MacroAssembler::call_VM()函数的汇编在之前已经详细介绍过,这里不再介绍,直接给出汇编代码,如下:
0x00007fffe101fd3a: callq 0x00007fffe101fd44 0x00007fffe101fd3f: jmpq 0x00007fffe101fdc2 0x00007fffe101fd44: mov %rbx,%rsi 0x00007fffe101fd47: lea 0x8(%rsp),%rax 0x00007fffe101fd4c: mov %r13,-0x38(%rbp) 0x00007fffe101fd50: mov %r15,%rdi 0x00007fffe101fd53: mov %rbp,0x200(%r15) 0x00007fffe101fd5a: mov %rax,0x1f0(%r15) 0x00007fffe101fd61: test $0xf,%esp 0x00007fffe101fd67: je 0x00007fffe101fd7f 0x00007fffe101fd6d: sub $0x8,%rsp 0x00007fffe101fd71: callq 0x00007ffff66b567c 0x00007fffe101fd76: add $0x8,%rsp 0x00007fffe101fd7a: jmpq 0x00007fffe101fd84 0x00007fffe101fd7f: callq 0x00007ffff66b567c 0x00007fffe101fd84: movabs $0x0,%r10 0x00007fffe101fd8e: mov %r10,0x1f0(%r15) 0x00007fffe101fd95: movabs $0x0,%r10 0x00007fffe101fd9f: mov %r10,0x200(%r15) 0x00007fffe101fda6: cmpq $0x0,0x8(%r15) 0x00007fffe101fdae: je 0x00007fffe101fdb9 0x00007fffe101fdb4: jmpq 0x00007fffe1000420 0x00007fffe101fdb9: mov -0x38(%rbp),%r13 0x00007fffe101fdbd: mov -0x30(%rbp),%r14 0x00007fffe101fdc1: retq
如上代码完成的事情很简单,就是调用C++函数编写的InterpreterRuntime::resolve_get_put()函数,此函数会填充常量池缓存中ConstantPoolCacheEntry信息,关于ConstantPoolCache以及ConstantPoolCacheEntry,还有ConstantPoolCacheEntry中各个字段的含义在《深入剖析Java虚拟机:源码剖析与实例详解(基础卷)》中已经详细介绍过,这里不再介绍。
InterpreterRuntime::resolve_get_put()函数的实现比较多,我们首先看一部分实现,如下:
IRT_