本文來自: PerfMa技術社區PerfMa(笨馬網絡)官網java
之因此會寫這個,主要是由於最近作的一個項目碰到了一個移位的問題,由於位移操做溢出致使結果不許確,原本能夠點到爲止,問題也能很快解決,可是不痛不癢的感受着實讓人不爽,因而深扒了下箇中細節,直到看到Intel的指令規約纔算釋然,但願這篇文章能引發你們共鳴。網絡
本文或許看起來會比較枯燥,不過其實認真看挺有意思的,若是實在看不下去,告訴你一個極簡路徑,先看下下面的Demo,而後直接跳到後面的小結,若是懂了,別忘記順便點個贊,請叫我雷鋒,哈哈。函數
仍是從一個簡單的例子提及spa
你們能夠嘗試作幾個改變,看看結果怎樣.net
4 << shift
改爲4L << shift
291=25+256*1
若是上面的各類結果你都能解釋,那說明你對位移操做仍是有必定了解的,不過本文主要從JVM到Intel X86_64指令角度來分析這個問題,或許也值得一看線程
要知道區別,咱們看doShiftL
方法經過javac編譯出來的指令有什麼不同3d
0: iconst_4 1: iload_0 2: ishl
0: ldc2_w #34 // long 4l 3: iload_0 4: lshl
針對4和4L的區別,咱們看到了兩條不一樣的指令,分別是iconst_4
和ldc2_w
,其實若是咱們將4改爲其餘的值,可能會有不同的指令出現code
-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爲例來簡單介紹下blog
先看iconst_4的大概彙編指令以下ip
重點看0x00007fcb529b0b30這條就是將0x4移到EAX寄存器裏,這是一個32位的寄存器,須要注意的是這裏並無直接將4 push到操做數棧上,而是在下一條指令(也就是iload_0)執行的時候才預先push到棧上,後面看iload_0的彙編代碼可知
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
的彙編實現大體以下:
這條指令簡單來講就是將方法的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
位置的指令
從上面的字節碼裏咱們看到,當咱們位移的基數是4或者4L的時候,分別看到了兩條不一樣的位移指令,分別是ishl
和lshl
,這兩條指令一個是將int型的值左移必定位數,一個是將long型的值左移必定位數,那這兩條指令分別有什麼區別呢?
先看定義
對於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寄存器,所以二者表示的最大值顯然不同啦
先看定義
lshl指令主要實如今lshl方法裏
而pop_l的實現以下,使用了movq,也就是移動棧上的雙字(8byte=64位,用RAX寄存器存)到寄存器裏,注意上面的ishl使用的是movl,是移動長字到寄存器裏(即4byte=32位,正好用EAX寄存器存),
lshl的彙編實現:
從這裏也印證了確實用了RAX寄存器(請看0x00007fcb529b59b1
)
這篇文章由於涉及到太多的彙編指令,可能很多人看起來不是很明白,不過我以爲你能夠多看幾遍啦,看多了也許就看懂了,不過實現看不下去不要緊,就看看小結吧
推薦閱讀: