【深入Java核心技术】从X86指令深扒JVM的位移操作

概述

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

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

Demo

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

public class ShiftTest {
    public static void main(String args[]) {
        System.out.println(doShiftL(35));
    }

    public static long doShiftL(int shift) {
        return 4 << shift;
    }
}

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

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

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

JVM里44L的区别

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

4 << shift的字节码

 0: iconst_4
 1: iload_0
 2: ishl

4L << shift的字节码

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

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

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

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

iconst_4

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

  0x00007fcb529b0b00: push   %rax
  0x00007fcb529b0b01: jmpq   0x00007fcb529b0b30
  0x00007fcb529b0b06: sub    $0x8,%rsp
  0x00007fcb529b0b0a: movss  %xmm0,(%rsp)
  0x00007fcb529b0b0f: jmpq   0x00007fcb529b0b30
  0x00007fcb529b0b14: sub    $0x10,%rsp
  0x00007fcb529b0b18: movsd  %xmm0,(%rsp)
  0x00007fcb529b0b1d: jmpq   0x00007fcb529b0b30
  0x00007fcb529b0b22: sub    $0x10,%rsp
  0x00007fcb529b0b26: mov    %rax,(%rsp)
  0x00007fcb529b0b2a: jmpq   0x00007fcb529b0b30
  0x00007fcb529b0b2f: push   %rax
  0x00007fcb529b0b30: mov    $0x4,%eax
  0x00007fcb529b0b35: movzbl 0x1(%r13),%ebx
  0x00007fcb529b0b3a: inc    %r13
  0x00007fcb529b0b3d: mov    $0x7fcb63dd5760,%r10
  0x00007fcb529b0b47: jmpq   *(%r10,%rbx,8)

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

ldc2_w

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

  0x00007fcb529b1960: push   %rax
  0x00007fcb529b1961: jmpq   0x00007fcb529b1990
  0x00007fcb529b1966: sub    $0x8,%rsp
  0x00007fcb529b196a: movss  %xmm0,(%rsp)
  0x00007fcb529b196f: jmpq   0x00007fcb529b1990
  0x00007fcb529b1974: sub    $0x10,%rsp
  0x00007fcb529b1978: movsd  %xmm0,(%rsp)
  0x00007fcb529b197d: jmpq   0x00007fcb529b1990
  0x00007fcb529b1982: sub    $0x10,%rsp
  0x00007fcb529b1986: mov    %rax,(%rsp)
  0x00007fcb529b198a: jmpq   0x00007fcb529b1990
  0x00007fcb529b198f: push   %rax
  0x00007fcb529b1990: movzwl 0x1(%r13),%ebx
  0x00007fcb529b1995: bswap  %ebx
  0x00007fcb529b1997: shr    $0x10,%ebx
  0x00007fcb529b199a: mov    -0x18(%rbp),%rcx
  0x00007fcb529b199e: mov    0x10(%rcx),%rcx
  0x00007fcb529b19a2: mov    0x8(%rcx),%rcx
  0x00007fcb529b19a6: mov    0x10(%rcx),%rax
  0x00007fcb529b19aa: cmpb   $0x6,0x4(%rax,%rbx,1)
  0x00007fcb529b19af: jne    0x00007fcb529b19c2
  0x00007fcb529b19b1: movsd  0x60(%rcx,%rbx,8),%xmm0
  0x00007fcb529b19b7: sub    $0x10,%rsp
  0x00007fcb529b19bb: movsd  %xmm0,(%rsp)
  0x00007fcb529b19c0: jmp    0x00007fcb529b19cf
  0x00007fcb529b19c2: mov    0x60(%rcx,%rbx,8),%rax
  0x00007fcb529b19c7: sub    $0x10,%rsp
  0x00007fcb529b19cb: mov    %rax,(%rsp)
  0x00007fcb529b19cf: movzbl 0x3(%r13),%ebx
  0x00007fcb529b19d4: add    $0x3,%r13
  0x00007fcb529b19d8: mov    $0x7fcb63dd7f60,%r10
  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的汇编实现大致如下:

  0x00007fcb529b1ee0: push   %rax
  0x00007fcb529b1ee1: jmpq   0x00007fcb529b1f10
  0x00007fcb529b1ee6: sub    $0x8,%rsp
  0x00007fcb529b1eea: movss  %xmm0,(%rsp)
  0x00007fcb529b1eef: jmpq   0x00007fcb529b1f10
  0x00007fcb529b1ef4: sub    $0x10,%rsp
  0x00007fcb529b1ef8: movsd  %xmm0,(%rsp)
  0x00007fcb529b1efd: jmpq   0x00007fcb529b1f10
  0x00007fcb529b1f02: sub    $0x10,%rsp
  0x00007fcb529b1f06: mov    %rax,(%rsp)
  0x00007fcb529b1f0a: jmpq   0x00007fcb529b1f10
  0x00007fcb529b1f0f: push   %rax
  0x00007fcb529b1f10: mov    (%r14),%eax
  0x00007fcb529b1f13: movzbl 0x1(%r13),%ebx
  0x00007fcb529b1f18: inc    %r13
  0x00007fcb529b1f1b: mov    $0x7fcb63dd5760,%r10
  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的时候,分别看到了两条不同的位移指令,分别是ishllshl,这两条指令一个是将int型的值左移一定位数,一个是将long型的值左移一定位数,那这两条指令分别有什么区别呢?

阅读全文请点击:点击打开链接

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值