从X86指令深扒JVM的位移操作
本文来自: PerfMa技术社区
概述
之所以会写这个,主要是因为最近做的一个项目碰到了一个移位的问题,因为位移操作溢出导致结果不准确,本来可以点到为止,问题也能很快解决,但是不痛不痒的感觉着实让人不爽,于是深扒了下个中细节,直到看到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发现结果是一样的
推荐阅读:
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
用Nginx实现接口慢查询并可示化展示TOP 20
相信很多小伙伴都见过一些商业产品中的url接口响应时间,实时汇总显示功能。可以理解为web接口的慢查询,与sql的慢查询有异曲同工之妙,但是想做却无从入手不知道怎么实现此功能,所以今天就教大家如何实现用grafana+nginx+mysql来实现此功能。 0x0 其实nginx本身就带有接口响应时间的功能,只不过还需要改造下,比如说单独记录超过1000ms(1秒)的响应,并写入数据库中。要注意的是并不建议大家将记录直接写入数据库中,因为数据库有时会成为nginx的负担,间接写入即可。需要简单修改下log模块,涉及文件ngx_http_log_module.c通常位于nginx-1.17.9/src/http/modules/ngx_http_log_module.c 大约838行,找到ngx_http_log_request_time函数并修改如下: static u_char * ngx_http_log_request_time(ngx_http_request_t *r, u_char *buf, ngx_http_log_op_t *op) { ngx_time_...
- 下一篇
在微服务框架Demo.MicroServer中添加SkyWalking+SkyApm-dotnet分布式链路追踪系统
1.APM工具的选取 Apm监测工具很多,这里选用网上比较火的一款Skywalking。 Skywalking是一个应用性能监控(APM)系统,Skywalking分为服务端Oap、管理界面UI、以及嵌入到程序中的探针Agent部分,大概工作流程就是在程序中添加探针采集各种数据发送给服务端保存,然后在UI界面可以看到收集过来的各种监测数据,来完成它的核心使命:性能监控和分布式调用链追踪能力。下图是skywalking官方的一个图,也可以说明这三者之间的关联关系 2.服务端(OAP)和界面(UI)的安装 这里直接在apache地址: http://skywalking.apache.org/downloads/ 下载了一个6.6.0版本的zip文件,由于之前在本地的windows上安装过,发现安装包里面有两个启动文件,分别为:startup.bat和startup.sh,分别用于window上启动和linux启动,这里我直接将之前下载好的上传到linux上来安装。 上传后解压缩,就会得到以下截图的几个文件 进入到config配置目录下面,有一个名称叫application.yml的文...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Linux系统CentOS6、CentOS7手动修改IP地址
- 2048小游戏-低调大师作品
- CentOS8安装Docker,最新的服务器搭配容器使用
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- Windows10,CentOS7,CentOS8安装Nodejs环境
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- 设置Eclipse缩进为4个空格,增强代码规范
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16