从X86指令深扒JVM的位移操作
概述
之所以会写这个,主要是因为最近做的一个项目碰到了一个移位的问题,因为位移操作溢出导致结果不准确,本来可以点到为止,问题也能很快解决,但是不痛不痒的感觉着实让人不爽,于是深扒了下个中细节,直到看到Intel的指令规约才算释然,希望这篇文章能引起大家共鸣。
本文或许看起来会比较枯燥,不过其实认真看挺有意思的,如果实在看不下去,告诉你一个极简路径,先看下下面的Demo,然后直接跳到后面的小结,如果懂了,别忘记顺便点个赞,请叫我雷锋,哈哈。
Demo
还是从一个简单的例子说起
大家可以尝试做几个改变,看看结果怎样
4 << shift
改成4L << shift
将35改成291,PS:提示一下
291=25+256*1
如果上面的各种结果你都能解释,那说明你对位移操作还是有一定了解的,不过本文主要从JVM到Intel X86_64指令角度来分析这个问题,或许也值得一看
JVM里4和4L的区别
要知道区别,我们看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
针对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的大概汇编指令如下
重点看0x00007fcb529b0b30这条就是将0x4移到EAX寄存器里,这是一个32位的寄存器,需要注意的是这里并没有直接将4 push到操作数栈上,而是在下一条指令(也就是iload_0)执行的时候才预先push到栈上,后面看iload_0的汇编代码可知
ldc2_w
ldc2_w是将long或者double的常量值从常量池推到操作数栈顶,其大概汇编指令如下
重点看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
的汇编实现大致如下:
这条指令简单来说就是将方法的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指令实现
先看定义
对于ishl
指令主要实现在iop2方法里,并且传递一个参数shl
因此主要实现其实就是
主要是将RAX寄存器里的值(其实就是doShiftL函数的shift参数)存入到RCX寄存器里(注意这里用的movl,其实是用的32位寄存器),然后将操作数栈顶的值(就是上述的4)存到RAX里,并做shll操作!
那问题就来了,这里的0xD3,0xE0到底是什么鬼,不过我们能猜到是做的位移操作,那我们看看ishl完整的汇编代码
上述的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指令这样的描述
0xD3的二进制表示是1101 0011
,和上面的1101 001w
是匹配的,这个w应该是如果是寄存器寻址,那就是1吧
0xE0的二进制表示是1110 0000
,和上面的11 100 reg
是匹配的,也就是reg占3位,那问题是寄存器个数并不只有8个,因此超过8个的情况怎么表示呢,那来看看encode的过程
这里的关键其实就是prefix的值了,通过设置prefix来看是否使用了普通寄存器之外的寄存器,这个大家网上可以找找相关资料看看,是X86的扩展64位技术
另外从上面的规范里我们看到了CL寄存器,也就是shl命令本身就是和CL寄存器紧密结合实现的(其中一种寻址方式而已),另外将shel之后的结果存到EAX寄存器里,再次提醒下是32位的寄存器,而和下面说的lshl的最大区别就是其使用的其实是64位的RAX寄存器,因此两者表示的最大值显然不一样啦
JVM里lshl指令实现
先看定义
lshl指令主要实现在lshl方法里
而pop_l的实现如下,使用了movq,也就是移动栈上的双字(8byte=64位,用RAX寄存器存)到寄存器里,注意上面的ishl使用的是movl,是移动长字到寄存器里(即4byte=32位,正好用EAX寄存器存),
lshl的汇编实现:
从这里也印证了确实用了RAX寄存器(请看0x00007fcb529b59b1
)
总结
这篇文章因为涉及到太多的汇编指令,可能不少人看起来不是很明白,不过我觉得你可以多看几遍啦,看多了也许就看懂了,不过实现看不下去没关系,就看看小结吧
当我们要位移的基数的类型是long的时候,其实是用64位的RAX寄存器来操作的,因此存的最大值(2^64-1)会更大,而如果基础是int的话,会用32位的EAX寄存器,因此能存的最大值(2^32-1)会小点,超过了阈值就会溢出
使用了8位的CL寄存器来存要位移的位数,因此最大其实就是2^8-1=255啦,所以上述demo,如果我们将shift的参数从35改成291发现结果是一样的
推荐阅读:
从X86指令深扒JVM的位移操作的更多相关文章
- 深扒JVM,对它进行“开膛破肚”式解析!
1. 打怪升级,你绕不开JVM JVM,对Java程序员进阶而言,是一个绝对绕不开,也不能绕开的话题. 在你打怪升级.进阶蜕变的路上,势必会遇到项目上线中各种OOM.GC等问题,此时JVM的功底就至关 ...
- 3.3 x86指令简介
计算机组成 3 指令系统体系结构 3.3 x86指令简介 x86指令种类繁多,数量庞大.在这一节我们将会学习x86指令的分类,并分析其中最为基础的一部分指令. 通常一个指令系统主要包括这几类指令.运算 ...
- 3.4 复杂的x86指令举例
计算机组成 3 指令系统体系结构 3.4 复杂的x86指令举例 x86作为复杂指令系统的代表,自然会有不少相当复杂的指令.在这一节我们将会看到其中有代表性的一些例子. 关于复杂的x86指令,我们这里举 ...
- 反汇编基本原理与x86指令构造
反汇编基本原理与x86指令构造 概要:旨在讲述程序的二进制代码转换到汇编.即反汇编的基本原理.以及 x86 架构的 CPU 的指令构造,有这个基础后就能够自己编写汇编程序了,也能够将二进制代码数据转换 ...
- WP8.1中C++的winodws运行时组件位移操作的差异
最近学习WP8.1应用开发,想把C语言的SM3国密算法移植到手机app中.由于把C语言的代码转换成C#代码工作量较大,因此只能用winodws运行时组件来实现. SM3国密算法是一种HASH算法,具体 ...
- 2.1(java编程思想笔记)位移操作
java位移操作主要有两种: 有符号位移:有符号位移会保留原有数字正负性,即正数依然是正数,负数依然是负数. 有符号位左移时,低位补0. 有符号右移时:当数字为正数,高位补0.当数字为负时高位补1. ...
- 一文带你深扒ClassLoader内核,揭开它的神秘面纱!
「MoreThanJava」 宣扬的是 「学习,不止 CODE」. 如果觉得 「不错」 的朋友,欢迎 「关注 + 留言 + 分享」,文末有完整的获取链接,您的支持是我前进的最大的动力! 前言 Clas ...
- 深挖Jvm垃圾收集
垃圾收集(Garbage Collection,GC),它的任务是解决以下 3 件问题: 哪些内存需要回收? 什么时候回收? 如何回收? 其中第一个问题很好回答,在 Java 中,GC 主要发生在 J ...
- 扒一扒JVM的垃圾回收机制,下次面试你准备好了吗
相信和小编一样的程序猿们在日常工作或面试当中经常会遇到JVM的垃圾回收问题,有没有在夜深人静的时候详细捋一捋JVM垃圾回收机制中的知识点呢?没时间捋也没关系,因为小编接下来会给你捋一捋. 一. 技 ...
随机推荐
- GitHub 热点速览 Vol.17:在?各家视频会员要不要?
作者:HelloGitHub-小鱼干 摘要:经济实用,用作上周的 GitHub 热点的横批再合适不过.先不说 GitHub Trending 上不止一个的会员共享项目,免你找好友刷脸要会员,这项目实在 ...
- view-controller
有的时候我们只想根据一个请求地址跳转到一个页面中,中间并没有任何的处理流程,这个时候创建一个 Controller 类再编写方法来跳转就显得很繁琐.这个时候我们就可以使用 view-controlle ...
- kubernetes (一)使用Rancher搭建集群
目录 如何快速高效部署K8s集群 Rancher是什么 为什么是Rancher 1.0.安装Rancher 1.1.环境 1.2.选择Rancher版本 1.3.拉取镜像 2.0.容器启动高级选项 2 ...
- [Qt] 去掉QMessageBox标题栏上的图标
msgBox.setWindowFlags(Qt::Drawer);
- (转)对 Linux 新手非常有用的 20 个命令
你打算从Windows换到Linux上来,还是你刚好换到Linux上来?哎哟!!!我说什么呢,是什么原因你就出现在我的世界里了.从我以往的经验来说,当我刚使用Linux,命令,终端啊什么的,吓了我一跳 ...
- eclipse 创建maven项目失败
问题描述: eclipse 初次创建maven项目报错 可能是maven-archetype-quickstart:1.1.jar 包失效了或者没有? 有人说把这个jar包放在maven本地仓库里 我 ...
- Spark 源码系列(六)Shuffle 的过程解析
Spark 大会上,所有的演讲嘉宾都认为 shuffle 是最影响性能的地方,但是又无可奈何.之前去百度面试 hadoop 的时候,也被问到了这个问题,直接回答了不知道. 这篇文章主要是沿着下面几个问 ...
- [New Portal]Windows Azure Web Site (3) 创建Web Site和云端数据库
<Windows Azure Platform 系列文章目录> 在前一章的内容里,我介绍了使用Windows Azure Management Portal创建Web Site.本章,我将 ...
- 由JS数组去重说起
一.问题描述: var array=[1,45,3,1,4,67,45],请编写一个函数reDup来去掉其中的重复项,即 reDup(array); console.log(array);//[1,4 ...
- 信息竞赛进阶指南--Tire树
// 假设字符串由小写字母构成 int trie[SIZE][26], tot = 1; // Trie的插入 void insert(char* str) { int len = strlen(st ...