從X86指令深扒JVM的位移操做

本文來自: PerfMa技術社區

PerfMa(笨馬網絡)官網java

概述

之因此會寫這個,主要是由於最近作的一個項目碰到了一個移位的問題,由於位移操做溢出致使結果不許確,原本能夠點到爲止,問題也能很快解決,可是不痛不癢的感受着實讓人不爽,因而深扒了下箇中細節,直到看到Intel的指令規約纔算釋然,但願這篇文章能引發你們共鳴。網絡

本文或許看起來會比較枯燥,不過其實認真看挺有意思的,若是實在看不下去,告訴你一個極簡路徑,先看下下面的Demo,而後直接跳到後面的小結,若是懂了,別忘記順便點個贊,請叫我雷鋒,哈哈。函數

Demo

仍是從一個簡單的例子提及spa

image.png

你們能夠嘗試作幾個改變,看看結果怎樣.net

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

若是上面的各類結果你都能解釋,那說明你對位移操做仍是有必定了解的,不過本文主要從JVM到Intel X86_64指令角度來分析這個問題,或許也值得一看線程

JVM裏4和4L的區別

要知道區別,咱們看doShiftL方法經過javac編譯出來的指令有什麼不同3d

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_4ldc2_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

先看iconst_4的大概彙編指令以下ip

image.png

重點看0x00007fcb529b0b30這條就是將0x4移到EAX寄存器裏,這是一個32位的寄存器,須要注意的是這裏並無直接將4 push到操做數棧上,而是在下一條指令(也就是iload_0)執行的時候才預先push到棧上,後面看iload_0的彙編代碼可知

ldc2_w

ldc2_w是將long或者double的常量值從常量池推到操做數棧頂,其大概彙編指令以下

image.png

重點看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的彙編實現大體以下:

image.png

這條指令簡單來講就是將方法的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型的值左移必定位數,那這兩條指令分別有什麼區別呢?

JVM裏ishl指令實現

先看定義

image.png

對於ishl指令主要實如今iop2方法裏,而且傳遞一個參數shl

image.png

所以主要實現其實就是

image.png

主要是將RAX寄存器裏的值(其實就是doShiftL函數的shift參數)存入到RCX寄存器裏(注意這裏用的movl,實際上是用的32位寄存器),而後將操做數棧頂的值(就是上述的4)存到RAX裏,並作shll操做!

image.png

那問題就來了,這裏的0xD3,0xE0究竟是什麼鬼,不過咱們能猜到是作的位移操做,那咱們看看ishl完整的彙編代碼

image.png

上述的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指令這樣的描述

image.png

0xD3的二進制表示是1101 0011,和上面的1101 001w是匹配的,這個w應該是若是是寄存器尋址,那就是1吧

0xE0的二進制表示是1110 0000,和上面的11 100 reg是匹配的,也就是reg佔3位,那問題是寄存器個數並不僅有8個,所以超過8個的狀況怎麼表示呢,那來看看encode的過程

image.png

這裏的關鍵其實就是prefix的值了,經過設置prefix來看是否使用了普通寄存器以外的寄存器,這個你們網上能夠找找相關資料看看,是X86的擴展64位技術

另外從上面的規範裏咱們看到了CL寄存器,也就是shl命令自己就是和CL寄存器緊密結合實現的(其中一種尋址方式而已),另外將shel以後的結果存到EAX寄存器裏,再次提醒下是32位的寄存器,而和下面說的lshl的最大區別就是其使用的實際上是64位的RAX寄存器,所以二者表示的最大值顯然不同啦

JVM裏lshl指令實現

先看定義

image.png

lshl指令主要實如今lshl方法裏

image.png

而pop_l的實現以下,使用了movq,也就是移動棧上的雙字(8byte=64位,用RAX寄存器存)到寄存器裏,注意上面的ishl使用的是movl,是移動長字到寄存器裏(即4byte=32位,正好用EAX寄存器存),

image.png

lshl的彙編實現:

image.png

從這裏也印證了確實用了RAX寄存器(請看0x00007fcb529b59b1)

總結

這篇文章由於涉及到太多的彙編指令,可能很多人看起來不是很明白,不過我以爲你能夠多看幾遍啦,看多了也許就看懂了,不過實現看不下去不要緊,就看看小結吧

  • 當咱們要位移的基數的類型是long的時候,實際上是用64位的RAX寄存器來操做的,所以存的最大值(2^64-1)會更大,而若是基礎是int的話,會用32位的EAX寄存器,所以能存的最大值(2^32-1)會小點,超過了閾值就會溢出
  • 使用了8位的CL寄存器來存要位移的位數,所以最大其實就是2^8-1=255啦,因此上述demo,若是咱們將shift的參數從35改爲291發現結果是同樣的

推薦閱讀

PerfMa KO 系列之 JVM 參數【Memory篇】

線程池運用不當的一次線上事故

相關文章
相關標籤/搜索