从X86指令深扒JVM的位移操作

服务器
之所以会写这个,主要是因为最近做的一个项目碰到了一个移位的问题,因为位移操作溢出导致结果不准确,本来可以点到为止,问题也能很快解决,但是不痛不痒的感觉着实让人不爽,于是深扒了下个中细节,直到看到Intel的指令规约才算释然,希望这篇文章能引起大家共鸣。

 概述

之所以会写这个,主要是因为最近做的一个项目碰到了一个移位的问题,因为位移操作溢出导致结果不准确,本来可以点到为止,问题也能很快解决,但是不痛不痒的感觉着实让人不爽,于是深扒了下个中细节,直到看到Intel的指令规约才算释然,希望这篇文章能引起大家共鸣。

本文或许看起来会比较枯燥,不过其实认真看挺有意思的,如果实在看不下去,告诉你一个极简路径,先看下下面的Demo,然后直接跳到后面的小结,如果懂了,别忘记顺便点个赞,请叫我雷锋,哈哈。

[[332033]]

Demo

还是从一个简单的例子说起

  1. public class ShiftTest { 
  2.     public static void main(String args[]) { 
  3.         System.out.println(doShiftL(35)); 
  4.     } 
  5.  
  6.     public static long doShiftL(int shift) { 
  7.         return 4 << shift; 
  8.     } 

大家可以尝试做几个改变,看看结果怎样

  • 4 << shift改成4L << shift
  • 将35改成291,PS:提示一下291=35+256*1

如果上面的各种结果你都能解释,那说明你对位移操作还是有一定了解的,不过本文主要从JVM到Intel X86_64指令角度来分析这个问题,或许也值得一看

JVM里4和4L的区别

要知道区别,我们看doShiftL方法通过javac编译出来的指令有什么不一样

4 << shift的字节码

  1. 0: iconst_4 
  2.  1: iload_0 
  3.  2: ishl 

4L << shift的字节码

  1. 0: ldc2_w        #34                 // long 4l 
  2.  3: iload_0 
  3.  4: lshl 

针对4和4L的区别,我们看到了两条不同的指令,分别是iconst_4和ldc2_w,其实如果我们将4改成其他的值,可能会有不一样的指令出现

  • -1<= x <=5: iconst_x
  • -128<= x <-1 || 5< x <=127:bipush
  • -32768 <= x < -128 || 127 < x <= 32767:sipush
  • -32768 > x || x > 32767:ldc

不过这些都不是我们今天的重点,不想细说了,就以iconst_4为例来简单介绍下

iconst_4

先看iconst_4的大概汇编指令如下

  1. 0x00007fcb529b0b00: push   %rax 
  2.   0x00007fcb529b0b01: jmpq   0x00007fcb529b0b30 
  3.   0x00007fcb529b0b06: sub    $0x8,%rsp 
  4.   0x00007fcb529b0b0a: movss  %xmm0,(%rsp) 
  5.   0x00007fcb529b0b0f: jmpq   0x00007fcb529b0b30 
  6.   0x00007fcb529b0b14: sub    $0x10,%rsp 
  7.   0x00007fcb529b0b18: movsd  %xmm0,(%rsp) 
  8.   0x00007fcb529b0b1d: jmpq   0x00007fcb529b0b30 
  9.   0x00007fcb529b0b22: sub    $0x10,%rsp 
  10.   0x00007fcb529b0b26: mov    %rax,(%rsp) 
  11.   0x00007fcb529b0b2a: jmpq   0x00007fcb529b0b30 
  12.   0x00007fcb529b0b2f: push   %rax 
  13.   0x00007fcb529b0b30: mov    $0x4,%eax 
  14.   0x00007fcb529b0b35: movzbl 0x1(%r13),%ebx 
  15.   0x00007fcb529b0b3a: inc    %r13 
  16.   0x00007fcb529b0b3d: mov    $0x7fcb63dd5760,%r10 
  17.   0x00007fcb529b0b47: jmpq   *(%r10,%rbx,8) 

重点看0x00007fcb529b0b30这条就是将0x4移到EAX寄存器里,这是一个32位的寄存器,需要注意的是这里并没有直接将4 push到操作数栈上,而是在下一条指令(也就是iload_0)执行的时候才预先push到栈上,后面看iload_0的汇编代码可知

ldc2_w

ldc2_w是将long或者double的常量值从常量池推到操作数栈顶,其大概汇编指令如下

  1. 0x00007fcb529b1960: push   %rax 
  2.  0x00007fcb529b1961: jmpq   0x00007fcb529b1990 
  3.  0x00007fcb529b1966: sub    $0x8,%rsp 
  4.  0x00007fcb529b196a: movss  %xmm0,(%rsp) 
  5.  0x00007fcb529b196f: jmpq   0x00007fcb529b1990 
  6.  0x00007fcb529b1974: sub    $0x10,%rsp 
  7.  0x00007fcb529b1978: movsd  %xmm0,(%rsp) 
  8.  0x00007fcb529b197d: jmpq   0x00007fcb529b1990 
  9.  0x00007fcb529b1982: sub    $0x10,%rsp 
  10.  0x00007fcb529b1986: mov    %rax,(%rsp) 
  11.  0x00007fcb529b198a: jmpq   0x00007fcb529b1990 
  12.  0x00007fcb529b198f: push   %rax 
  13.  0x00007fcb529b1990: movzwl 0x1(%r13),%ebx 
  14.  0x00007fcb529b1995: bswap  %ebx 
  15.  0x00007fcb529b1997: shr    $0x10,%ebx 
  16.  0x00007fcb529b199a: mov    -0x18(%rbp),%rcx 
  17.  0x00007fcb529b199e: mov    0x10(%rcx),%rcx 
  18.  0x00007fcb529b19a2: mov    0x8(%rcx),%rcx 
  19.  0x00007fcb529b19a6: mov    0x10(%rcx),%rax 
  20.  0x00007fcb529b19aa: cmpb   $0x6,0x4(%rax,%rbx,1) 
  21.  0x00007fcb529b19af: jne    0x00007fcb529b19c2 
  22.  0x00007fcb529b19b1: movsd  0x60(%rcx,%rbx,8),%xmm0 
  23.  0x00007fcb529b19b7: sub    $0x10,%rsp 
  24.  0x00007fcb529b19bb: movsd  %xmm0,(%rsp) 
  25.  0x00007fcb529b19c0: jmp    0x00007fcb529b19cf 
  26.  0x00007fcb529b19c2: mov    0x60(%rcx,%rbx,8),%rax 
  27.  0x00007fcb529b19c7: sub    $0x10,%rsp 
  28.  0x00007fcb529b19cb: mov    %rax,(%rsp) 
  29.  0x00007fcb529b19cf: movzbl 0x3(%r13),%ebx 
  30.  0x00007fcb529b19d4: add    $0x3,%r13 
  31.  0x00007fcb529b19d8: mov    $0x7fcb63dd7f60,%r10 
  32.  0x00007fcb529b19e2: jmpq   *(%r10,%rbx,8) 

重点看0x00007fcb529b1990这条开始,主要就是从常量池里取出相关的值,然后push到操作数栈上(看0x00007fcb529b19c2这行开始的接下来三行)

因此做一个小结:

  • iconst_4:将4存入到EAX寄存器,但是此时还并没有将4 push到操作数栈顶
  • ldc2_w:将后面跟着的值(其实也就会4),存到RAX寄存器,并且将其push到操作数栈顶

着重注意下上面两条指令使用的两个寄存器是不一样的,一个是EAX,一个是RAX,其中RAX是64位寄存器,而EAX是RAX寄存器的低32位,是一个32位寄存器

不过还没结束,对于iconst_4这种情况,什么时候将4 push到栈上呢,那接下来我们看看iload_0这条指令,因为不管是iconst_4还是ldc2_w,后面都跟了iload_0,所以还是一起来看看这条指令

iload_0

iload_0的汇编实现大致如下:

  1. 0x00007fcb529b1ee0: push   %rax 
  2.   0x00007fcb529b1ee1: jmpq   0x00007fcb529b1f10 
  3.   0x00007fcb529b1ee6: sub    $0x8,%rsp 
  4.   0x00007fcb529b1eea: movss  %xmm0,(%rsp) 
  5.   0x00007fcb529b1eef: jmpq   0x00007fcb529b1f10 
  6.   0x00007fcb529b1ef4: sub    $0x10,%rsp 
  7.   0x00007fcb529b1ef8: movsd  %xmm0,(%rsp) 
  8.   0x00007fcb529b1efd: jmpq   0x00007fcb529b1f10 
  9.   0x00007fcb529b1f02: sub    $0x10,%rsp 
  10.   0x00007fcb529b1f06: mov    %rax,(%rsp) 
  11.   0x00007fcb529b1f0a: jmpq   0x00007fcb529b1f10 
  12.   0x00007fcb529b1f0f: push   %rax 
  13.   0x00007fcb529b1f10: mov    (%r14),%eax 
  14.   0x00007fcb529b1f13: movzbl 0x1(%r13),%ebx 
  15.   0x00007fcb529b1f18: inc    %r13 
  16.   0x00007fcb529b1f1b: mov    $0x7fcb63dd5760,%r10 
  17.   0x00007fcb529b1f25: jmpq   *(%r10,%rbx,8) 

这条指令简单来说就是将方法的0号local槽里的数据存到EAX寄存器里,不过针对上一条指令是iconst_4,此时会先做一个push的动作,将RAX寄存器里的值push到操作数栈上,但是如果是ldc2_w指令的话,就不会做push了,因为这两条指令规定的执行完后的top of stack不一样,iconst_4要求栈顶是一个int,而ldc2_w没要求,尽管在实现里确实将值push到了栈顶

因此在执行完iload_0之后,都已经将4 push到操作数栈顶了,并且将第一个local槽,其实就是doShiftL函数的shift参数存到了EAX寄存器里,具体看上面的0x00007fcb529b1f0f位置的指令

JVM里的位移操作

从上面的字节码里我们看到,当我们位移的基数是4或者4L的时候,分别看到了两条不同的位移指令,分别是ishl和lshl,这两条指令一个是将int型的值左移一定位数,一个是将long型的值左移一定位数,那这两条指令分别有什么区别呢?

JVM里ishl指令实现

先看定义

  1. def(Bytecodes::_ishl                , ____|____|____|____, itos, itos, iop2                , shl          ); 

对于ishl指令主要实现在iop2方法里,并且传递一个参数shl

  1. void TemplateTable::iop2(Operation op) { 
  2.   transition(itos, itos); 
  3.   switch (op) { 
  4.   case add  :                    __ pop_i(rdx); __ addl (rax, rdx); break; 
  5.   case sub  : __ movl(rdx, rax); __ pop_i(rax); __ subl (rax, rdx); break; 
  6.   case mul  :                    __ pop_i(rdx); __ imull(rax, rdx); break; 
  7.   case _and :                    __ pop_i(rdx); __ andl (rax, rdx); break; 
  8.   case _or  :                    __ pop_i(rdx); __ orl  (rax, rdx); break; 
  9.   case _xor :                    __ pop_i(rdx); __ xorl (rax, rdx); break; 
  10.   case shl  : __ movl(rcx, rax); __ pop_i(rax); __ shll (rax);      break; 
  11.   case shr  : __ movl(rcx, rax); __ pop_i(rax); __ sarl (rax);      break; 
  12.   case ushr : __ movl(rcx, rax); __ pop_i(rax); __ shrl (rax);      break; 
  13.   default   : ShouldNotReachHere(); 
  14.   } 

因此主要实现其实就是

  1. __ movl(rcx, rax); __ pop_i(rax); __ shll (rax);  

主要是将RAX寄存器里的值(其实就是doShiftL函数的shift参数)存入到RCX寄存器里(注意这里用的movl,其实是用的32位寄存器),然后将操作数栈顶的值(就是上述的4)存到RAX里,并做shll操作

  1. void Assembler::shll(Register dst) { 
  2.   int encode = prefix_and_encode(dst->encoding()); 
  3.   emit_byte(0xD3); 
  4.   emit_byte(0xE0 | encode); 

那问题就来了,这里的0xD3,0xE0到底是什么鬼,不过我们能猜到是做的位移操作,那我们看看ishl完整的汇编代码

  1. 0x00007fcb529b5920: mov    (%rsp),%eax 
  2.   0x00007fcb529b5923: add    $0x8,%rsp 
  3.   0x00007fcb529b5927: mov    %eax,%ecx 
  4.   0x00007fcb529b5929: mov    (%rsp),%eax 
  5.   0x00007fcb529b592c: add    $0x8,%rsp 
  6.   0x00007fcb529b5930: shl    %cl,%eax 
  7.   0x00007fcb529b5932: movzbl 0x1(%r13),%ebx 
  8.   0x00007fcb529b5937: inc    %r13 
  9.   0x00007fcb529b593a: mov    $0x7fcb63dd5760,%r10 
  10.   0x00007fcb529b5944: jmpq   *(%r10,%rbx,8) 

上述的0x00007fcb529b5930其实就应该是上面的Assembler::shll的输出了,里面有CL寄存器(RCX寄存器的低32位是ECX,而ECX的低8位是CL,这个关系清楚了吧)和EAX寄存器,看到这指令其实可以解释了,CL寄存器因为是ECX寄存器的低8位,而我们从上面得知RCX里存的其实是要位移的位数,也就是上面Demo里的doShiftL函数的shift参数值,而EAX寄存器里的值是操作数栈顶的值,也就是4

那现在的问题是明明我们就传了一个RAX的寄存器给Assembler::shll,那怎么操作起CL寄存器来了,这其实就是我想写本文的根本原因,我想解释这个现象,还想知道0xD3,0xE0到底是什么鬼,于是找了intel指令手册,看到SHL指令这样的描述

  1. register by CL           1101 001w : 11 100 reg 

0xD3的二进制表示是1101 0011,和上面的1101 001w是匹配的,这个w应该是如果是寄存器寻址,那就是1吧

0xE0的二进制表示是1110 0000,和上面的11 100 reg是匹配的,也就是reg占3位,那问题是寄存器个数并不只有8个,因此超过8个的情况怎么表示呢,那来看看encode的过程

  1. int Assembler::prefix_and_encode(int reg_enc, bool byteinst) { 
  2.   if (reg_enc >= 8) { 
  3.     prefix(REX_B); 
  4.     reg_enc -= 8; 
  5.   } else if (byteinst && reg_enc >= 4) { 
  6.     prefix(REX); 
  7.   } 
  8.   return reg_enc; 

这里的关键其实就是prefix的值了,通过设置prefix来看是否使用了普通寄存器之外的寄存器,这个大家网上可以找找相关资料看看,是X86的扩展64位技术

另外从上面的规范里我们看到了CL寄存器,也就是shl命令本身就是和CL寄存器紧密结合实现的(其中一种寻址方式而已),另外将shel之后的结果存到EAX寄存器里,再次提醒下是32位的寄存器,而和下面说的lshl的最大区别就是其使用的其实是64位的RAX寄存器,因此两者表示的最大值显然不一样啦

JVM里lshl指令实现

先看定义

  1. def(Bytecodes::_lshl                , ____|____|____|____, itos, ltos, lshl                ,  _           ); 

lshl指令主要实现在lshl方法里

  1. void TemplateTable::lshl() { 
  2.   transition(itos, ltos); 
  3.   __ movl(rcx, rax);                             // get shift count 
  4.   __ pop_l(rax);                                 // get shift value 
  5.   __ shlq(rax); 

而pop_l的实现如下,使用了movq,也就是移动栈上的双字(8byte=64位,用RAX寄存器存)到寄存器里,注意上面的ishl使用的是movl,是移动长字到寄存器里(即4byte=32位,正好用EAX寄存器存),

  1. void InterpreterMacroAssembler::pop_l(Register r) { 
  2.   movq(r, Address(rsp, 0)); 
  3.   addptr(rsp, 2 * Interpreter::stackElementSize); 

lshl的汇编实现:

  1. 0x00007fcb529b59a0: mov    (%rsp),%eax 
  2.   0x00007fcb529b59a3: add    $0x8,%rsp 
  3.   0x00007fcb529b59a7: mov    %eax,%ecx 
  4.   0x00007fcb529b59a9: mov    (%rsp),%rax 
  5.   0x00007fcb529b59ad: add    $0x10,%rsp 
  6.   0x00007fcb529b59b1: shl    %cl,%rax 
  7.   0x00007fcb529b59b4: movzbl 0x1(%r13),%ebx 
  8.   0x00007fcb529b59b9: inc    %r13 
  9.   0x00007fcb529b59bc: mov    $0x7fcb63dd5f60,%r10 
  10.   0x00007fcb529b59c6: jmpq   *(%r10,%rbx,8) 

从这里也印证了确实用了RAX寄存器(请看0x00007fcb529b59b1)

总结

这篇文章因为涉及到太多的汇编指令,可能不少人看起来不是很明白,不过我觉得你可以多看几遍啦,看多了也许就看懂了,不过实现看不下去没关系,就看看小结吧

当我们要位移的基数的类型是long的时候,其实是用64位的RAX寄存器来操作的,因此存的最大值(2^64-1)会更大,而如果基础是int的话,会用32位的EAX寄存器,因此能存的最大值(2^32-1)会小点,超过了阈值就会溢出

使用了8位的CL寄存器来存要位移的位数,因此最大其实就是2^8-1=255啦,所以上述demo,如果我们将shift的参数从35改成291发现结果是一样的

 

责任编辑:武晓燕 来源: 今日头条
相关推荐

2010-05-07 17:47:12

Unix Solari

2012-12-24 09:27:49

企业关键RISCx86

2011-12-12 10:24:29

X86RISC迁移

2009-02-16 18:31:20

服务器虚拟化VMware虚拟化

2011-12-01 11:09:48

AMDx86服务器英特尔

2012-01-04 09:59:50

2010-04-30 11:25:35

龙芯超龙计划

2010-02-05 16:04:45

X86 Android

2011-01-24 15:04:01

Windows Pho

2011-12-13 10:03:05

RISCX86平台服务器

2013-08-08 11:01:11

谷歌Android 4.3

2011-04-13 23:15:17

英特尔IDFX86

2014-02-13 14:59:03

方物

2011-11-10 09:26:48

Solaris 11

2009-08-28 14:38:33

2011-12-19 10:55:58

云计算中国电信

2011-02-20 22:23:43

X86虚拟化XenServer

2013-01-14 10:08:10

2019-03-22 08:25:20

x86PythonARM

2010-02-04 16:27:24

Android X86
点赞
收藏

51CTO技术栈公众号