之所以写这个是因为在最近的项目中遇到了shift的问题,由于shift操作溢出导致结果不准确。我本可以在这里停下来。问题也能很快解决,但是那种不痛不痒的感觉真的很烦人,所以我在下一步深挖细节,直到看到intel的指令协议才放下心来。希望这篇文章能引起大家的共鸣。这篇文章看似枯燥,但仔细阅读其实还是很有趣的。如果你实在看不下去了,我就告诉你一条极简之路。先看下面的demo,然后直接跳到后面的总结。如果你懂了,别忘了顺便点个赞,请叫我雷锋,哈哈。Demo从一个简单的例子开始结果怎么是4<x||X>32767:LDC,但这些不是我们今天的重点,我不想详细介绍,只需将ICONST_4作为例来例来简单简单简单介绍介绍介绍介绍介绍介绍介绍介绍先iConst_4iconst_4iconst_4看的的的的的的的大概大概汇编指令指令汇编汇编指令汇编指令指令指令汇编指令指令指令0x000077fcb529b00:$0x8,%rsp0x00007fcb529b0b0a:movss%xmm0,(%rsp)0x00007fcb529b0b0f:jmpq0x00007fcb529b0b300x00007fcb529b0b14:sub$0x10,%rsp0x00007fcb529b0b18:movsd%xmm0,(%rsp)0x00007fcb529b0b1d:jmpq0x00007fcb529b0b300x00007fcb529b0b22:sub$0x10,%rsp0x00007fcb529b0b26:mov%rax,(%rsp)0x00007fcb529b0b2a:jmpq0x00007fcb529b0b300x00007fcb529b0b2f:push%rax0x00007fcb529b0b30:mov$0x4,%eax0x00007fcb529b0b35:movzbl0x1(%r13),%ebx0x00007fcb529b0b3a:inc%r130x00007fcb529b0b3d:mov$0x7fcb63dd5760,%r100x00007fcb529b0b47:jmpq*(%r10,%rbx,8)重点看0x00007fcb529b0b30这条就是将0x4移到EAX寄存器里,这是一个32Itshouldbenotedthat4isnotdirectlypushedtotheoperandstack,butispushedtothestackinadvancewhenthenextinstruction(thatis,iload_0)isexecuted.Lookingattheassemblycodeofiload_0later,wecanseethatldc2_wldc2_wis将long或者double的常量值从常量池推到操作数栈顶,其大概汇编指令如下0x00007fcb529b1960:push%rax0x00007fcb529b1961:jmpq0x00007fcb529b19900x00007fcb529b1966:sub$0x8,%rsp0x00007fcb529b196a:movss%xmm0,(%rsp)0x00007fcb529b196f:jmpq0x00007fcb529b19900x00007fcb529b1974:sub$0x10,%rsp0x00007fcb529b1978:movsd%xmm0,(%rsp)0x00007fcb529b197d:jmpq0x00007fcb529b19900x00007fcb529b1982:sub$0x10,%rsp0x00007fcb560,xmovb1980007fcb529b198a:jmpq0x00007fcb529b19900x00007fcb529b198f:push%rax0x00007fcb529b1990:movzwl0x1(%r13),%ebx0x00007fcb529b1995:bswap%ebx0x00007fcb529b1997:shr$0x10,%ebx0x00007fcb529b199a:mov-0x18(%rbp),%rcx0x00007fcb529b199e:mov0x10(%rcx),%rcx0x00007fcb529b19a2:mov0x8(%rcx),%rcx0x00007fcb529b19a6:mov0x10(%rcx),%rax0x00007fcb529b19aa:cmpb$0x6,0x4(%rax,%rbx,1)0x00007fcb529b19af:jne0x00007fcb529b19c20x00007fcb529b19b1:movsd0x60(%rcx,%rbx,8),%xmm00x00007fcb529b19b7:sub$0x10,%rsp0x00007fcb529b19bb:movsd%xmm0,(%rsp)0x00007fcb529b19c0:jmp0x00007fcb529b19cf0x00007fcb529b19c2:mov0x60(%rcx,%rbx,8),%rax0x00007fcb529b19c7:sub$0x10,%rsp0x00007fcb529b19cb:mov%rax,(%rsp)0x00007fcb529b19cf:movzbl0x3(%r13),%ebx0x00007fcb529b19d4:add$0x3,%r130x00007fcb529b19d8:mov$0x7fcb63dd7f60,%r100x00007fcb529b19e2:jmpq*(%r10,%rbx,8)重点看0x00007fcb529b1990这条开始,主要就是从常量池里取出相关的值,Thenpushtotheoperandstack(seethenextthreelinesstartingfromtheline0x00007fcb529b19c2),somakeasummary:iconst_4:store4intotheEAXregister,butatthistime,4hasnotbeenpushedtothetopoftheoperandstackldc2_w:WillThefollowingvalue(actually4)isstoredintheRAXregisterandpushedtothetopoftheoperandstack.Notethatthetworegistersusedbytheabovetwoinstructionsaredifferent,oneisEAXandtheotherisRAX,whereRAXisa64-bitregister,andEAXisthelower32bitsoftheRAXregister,whichisa32-bitregisterbutit’snotoveryet.Forthecaseoficonst_4,whenwill4bepushedtothestack?Thenlet’sseeiload_0这条指令,因为不管是iconst_4还是ldc2_w,后面都跟了iload_0,所以还是一起来看看这条指令iload_0iload_0的汇编实现大致如下:0x00007fcb529b1ee0:push%rax0x00007fcb529b1ee1:jmpq0x00007fcb529b1f100x00007fcb529b1ee6:sub$0x8,%rsp0x00007fcb529b1eea:movss%xmm0,(%rsp)0x00007fcb529b1eef:jmpq0x00007fcb529b1f100x00007fcb529b1ef4:sub$0x10,%rsp0x00007fcb529b1ef8:movsd%xmm0,(%rsp)0x00007fcb529b1efd:jmpq0x00007fcb529b1f100x00007fcb529b1f02:sub$0x10,%rsp0x00007fcb529b1f06:mov%rax,(%rsp)0x00007fcb529b1f0a:jmpq0x00007fcb529b1f100x00007fcb529b1f0f:push%rax0x00007fcb529b1f10:mov(%r14),%eax0x00007fcb529b1f13:movzbl0x1(%r13),%ebx0x00007fcb529b1f18:inc%r130x00007fcb529b1f1b:mov$0x7fcb63dd5760,%r100x00007fcb529b1f25:jmpq*(%r10,%rbx,8)这条指令简单来说就是Storethedatainlocalslot0ofthemethodintotheEAXregister,butforthelastinstructionisiconst_4,apushactionwillbeperformedfirsttopushthevalueintheRAXregistertotheoperandstack,butifitisIftheldc2_wcommandisused,itwillnotdopush,because这两条指令执行后指定的栈顶是不同的。iconst_4要求栈顶是int,而ldc2_w不要求,虽然在实现中确实是push值到栈顶,所以执行完iload_0后,已经是Push4到栈顶了操作数入栈,并将第一个局部槽,也就是doShiftL函数的移位参数存入EAX寄存器。具体可以从上面的字节码看上面0x00007fcb529b1f0f位置的指令在JVM中的位移操作这里我们看到,当我们位移的基数是4或者4L的时候,我们看到了两条不同的位移指令,分别是ishl和lshl。这两条指令之一是将int值向左移动一定位数。一种是将long类型的值左移一定位数。两条指令有什么区别?ishl指令在JVM中的实现首先看定义def(Bytecodes::_ishl,____|____|____|____,itos,itos,iop2,shl);对于ishl指令,主要是在iop2方法中实现,一个参数shlvoidTemplateTable::iop2(Operationop){transition(itos,itos);switch(op){caseadd:__pop_i(rdx);__addl(rax,rdx);break;casesub:__movl(rdx,rax);__pop_i(rax);__subl(rax,rdx);break;casemul:__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;caseshl:__movl(rcx,rax);__pop_i(rax);__shll(rax);break;caseshr:__movl(rcx,rax);__pop_i(rax);__sarl(rax);break;caseushr:__movl(rcx,rax);__pop_i(rax);__shrl(rax);打破;默认:ShouldNotReachHere();}}所以主要实现其实是__movl(rcx,rax);__pop_i(rax);__shll(rax);主要是将RAX寄存器中的值(实际上是doShiftL函数的shift参数)存入RCX寄存器(注意这里使用的movl实际上是32位寄存器),然后将值存入操作数的最前面在RAX中栈(即上述4),并进行一次shll操作voidAssembler::shll(Registerdst){intencode=prefix_and_encode(dst->encoding());emit_byte(0xD3);emit_byte(0xE0|encode);}那么问题来了,这里的0xD3和0xE0是什么鬼,不过我们可以猜到是位移操作,那我们看ishl完整的汇编代码0x00007fcb529b5920:mov(%rsp),%eax0x00007fcb529b5923:add$0x8,%rsp0x00007fcb529b5927:mov%eax,%ecx0x00007fcb529b5929:mov(%rsp),%eax0x00007fcb529b592c:add$0x8,%rsp0x00007fcb529b5930:shl%cl,%eax0x00007fcb529b5932:movzbl0x1(%r13),%ebx0x00007fcb529b5937:inc%r130x00007fcb529b593a:mov$0x7fcb63dd5760,%r100x00007fcb529b5944:jmpq*(%r10,%rbx,8)上面描述的0x00007fcb529b5930其实际上应该是上面的Assembler::shll输出,有CL寄存器(低32位的RCX寄存器是ECX,而ECX的低8位是CL,这个关系就清楚了)和EAX寄存器,看到这条指令其实就可以解释了,因为CL寄存器就是ECX寄存器的低8位,而我们知道由上可知RCX中存放的其实是要移位的位数,也就是上面demo中doShiftL函数的shift参数值,而EAX寄存器中的值就是操作数栈顶的值,也就是4。现在的问题是我们给Assembler::shll传了一个RAX寄存器,那么如何操作CL寄存器呢。这其实也是我写这篇文章的根本原因。我想解释一下这是一个现象,我还是想知道0xD3和0xE0是什么,于是找了intel指令手册,看到CL1101001w对SHL指令寄存器的描述:11100reg0xD3的二进制表示是11010011,与上面的1101相符001w,这个w应该是如果是寄存器寻址的话就是1。0xE0的二进制表示是11100000,和上面的11100reg相匹配,即reg占3位。问题是寄存器的个数不是只有8个,所以超过8个的情况怎么表示,我们看看encode的过程){prefix(REX);}returnreg_enc;}这里的关键其实是前缀的值。通过设置前缀看是否使用了普通寄存器以外的寄存器,网上可以查到相关资料,是X86Extended64-bittechnology另外从上面的规范我们可以看到CL寄存器,也就是shl命令本身与CL寄存器(寻址方式之一)紧密结合,shel后的结果存放在EAX寄存器中,再次提醒一下,它是一个32位的寄存器,和前面提到的lshl最大的区别下面是它实际上使用了一个64位的RAX寄存器,所以两者的最大值明显不同。lshl指令在JVM中的实现首先看定义def(Bytecodes::_lshl,____|____|____|____,itos,ltos,lshl,_);lshl指令主要在lshl方法voidTemplateTable::lshl(){transition(itos,ltos);__movl(rcx,rax)中实现。,注意上面的ishl用的是movl,就是把长字搬进寄存器(也就是4byte=32位,存放在EAX寄存器),voidInterpreterMacroAssembler::pop_l(Registerr){movq(r,Address(rsp,0));addptr(rsp,2*Interpreter::stackElementSize);}lshl的汇编实现:0x00007fcb529b59a0:mov(%rsp),%eax0x00007fcb529b59a3:add$0x8,%rsp0x00007fcb529b59a7:mov%eax,%ecx0x00007fcb529b59a9:mov(%rsp),%rax0x00007fcb529b59ad:add$0x10,%rsp0x00007fcb529b59b1:shl%cl,%rax0x00007fcb529b59b4:movzbl0x1(%r13),%ebx0x00007fcb529b59b9:inc%r130x00007fcb529b59bc:mov$0x7fcb63dd5f60,%r100x00007fcb529b59c6: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,发现结果是一样的