第21篇-虚拟机字节码之运算指令
虚拟机规范中与运算相关的字节码指令如下表所示。
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 --
如上汇编代码的逻辑非常简单,这里不再介绍。
关于其它字节码指令的逻辑也相对简单,有兴趣的可自行研究。
推荐阅读:
第2篇-JVM虚拟机这样来调用Java主类的main()方法
第13篇-通过InterpreterCodelet存储机器指令片段
第20篇-加载与存储指令之ldc与_fast_aldc指令(2)
如果有问题可直接评论留言或加作者微信mazhimazh
关注公众号,有HotSpot VM源码剖析系列文章!