第27篇-虛擬機位元組碼指令之控制轉移指令

  • 2021 年 9 月 27 日
  • 筆記

控制轉移相關的位元組碼指令如下表所示。 

0x99

ifeq

當棧頂int型數值等於0時跳轉

0x9a

ifne

當棧頂int型數值不等於0時跳轉

0x9b

iflt

當棧頂int型數值小於0時跳轉

0x9c

ifge

當棧頂int型數值大於等於0時跳轉

0x9d

ifgt

當棧頂int型數值大於0時跳轉

0x9e

ifle

當棧頂int型數值小於等於0時跳轉

0x9f

if_icmpeq

比較棧頂兩int型數值大小,當結果等於0時跳轉

0xa0

if_icmpne

比較棧頂兩int型數值大小,當結果不等於0時跳轉

0xa1

if_icmplt

比較棧頂兩int型數值大小,當結果小於0時跳轉

0xa2

if_icmpge

比較棧頂兩int型數值大小,當結果大於等於0時跳轉

0xa3

if_icmpgt

比較棧頂兩int型數值大小,當結果大於0時跳轉

0xa4

if_icmple

比較棧頂兩int型數值大小,當結果小於等於0時跳轉

0xa5

if_acmpeq

比較棧頂兩引用型數值,當結果相等時跳轉

0xa6

if_acmpne

比較棧頂兩引用型數值,當結果不相等時跳轉

0xa7

goto

無條件跳轉

0xa8

jsr

跳轉至指定16offset位置,並將jsr下一條指令地址壓入棧頂

0xa9

ret

返回至本地變數指令的index的指令位置(一般與jsr或jsr_w聯合使用)

0xaa

tableswitch

用於switch條件跳轉,case值連續(可變長度指令)

0xab

lookupswitch

用於switch條件跳轉,case值不連續(可變長度指令)

0xac

ireturn

從當前方法返回int

0xad

lreturn

從當前方法返回long

0xae

freturn

從當前方法返回float

0xaf

dreturn

從當前方法返回double

0xb0

areturn

從當前方法返回對象引用

0xb1

return

從當前方法返回void

0xc6

ifnull

null時跳轉

0xc7

ifnonnull

不為null時跳轉

0xc8

goto_w

無條件跳轉(寬索引)

0xc9

jsr_w

跳轉至指定32offset位置,並將jsr_w下一條指令地址壓入棧頂

 

模板定義如下:

def(Bytecodes::_ifeq                , ubcp|____|clvm|____, itos, vtos, if_0cmp             , equal        );
def(Bytecodes::_ifne                , ubcp|____|clvm|____, itos, vtos, if_0cmp             , not_equal    );
def(Bytecodes::_iflt                , ubcp|____|clvm|____, itos, vtos, if_0cmp             , less         );
def(Bytecodes::_ifge                , ubcp|____|clvm|____, itos, vtos, if_0cmp             , greater_equal);
def(Bytecodes::_ifgt                , ubcp|____|clvm|____, itos, vtos, if_0cmp             , greater      );
def(Bytecodes::_ifle                , ubcp|____|clvm|____, itos, vtos, if_0cmp             , less_equal   );
def(Bytecodes::_if_icmpeq           , ubcp|____|clvm|____, itos, vtos, if_icmp             , equal        );
def(Bytecodes::_if_icmpne           , ubcp|____|clvm|____, itos, vtos, if_icmp             , not_equal    );
def(Bytecodes::_if_icmplt           , ubcp|____|clvm|____, itos, vtos, if_icmp             , less         );
def(Bytecodes::_if_icmpge           , ubcp|____|clvm|____, itos, vtos, if_icmp             , greater_equal);
def(Bytecodes::_if_icmpgt           , ubcp|____|clvm|____, itos, vtos, if_icmp             , greater      );
def(Bytecodes::_if_icmple           , ubcp|____|clvm|____, itos, vtos, if_icmp             , less_equal   );
def(Bytecodes::_if_acmpeq           , ubcp|____|clvm|____, atos, vtos, if_acmp             , equal        );
def(Bytecodes::_if_acmpne           , ubcp|____|clvm|____, atos, vtos, if_acmp             , not_equal    );
def(Bytecodes::_goto                , ubcp|disp|clvm|____, vtos, vtos, _goto               ,  _           );
def(Bytecodes::_jsr                 , ubcp|disp|____|____, vtos, vtos, jsr                 ,  _           ); // result is not an oop, so do not transition to atos
def(Bytecodes::_ret                 , ubcp|disp|____|____, vtos, vtos, ret                 ,  _           );
def(Bytecodes::_tableswitch         , ubcp|disp|____|____, itos, vtos, tableswitch         ,  _           );
def(Bytecodes::_lookupswitch        , ubcp|disp|____|____, itos, itos, lookupswitch        ,  _           );
def(Bytecodes::_ireturn             , ____|disp|clvm|____, itos, itos, _return             , itos         );
def(Bytecodes::_lreturn             , ____|disp|clvm|____, ltos, ltos, _return             , ltos         );
def(Bytecodes::_freturn             , ____|disp|clvm|____, ftos, ftos, _return             , ftos         );
def(Bytecodes::_dreturn             , ____|disp|clvm|____, dtos, dtos, _return             , dtos         );
def(Bytecodes::_areturn             , ____|disp|clvm|____, atos, atos, _return             , atos         );
def(Bytecodes::_return              , ____|disp|clvm|____, vtos, vtos, _return             , vtos         );

def(Bytecodes::_ifnull              , ubcp|____|clvm|____, atos, vtos, if_nullcmp          , equal        );
def(Bytecodes::_ifnonnull           , ubcp|____|clvm|____, atos, vtos, if_nullcmp          , not_equal    );
def(Bytecodes::_goto_w              , ubcp|____|clvm|____, vtos, vtos, goto_w              ,  _           );
def(Bytecodes::_jsr_w               , ubcp|____|____|____, vtos, vtos, jsr_w               ,  _           );

下面介紹幾個典型指令的彙編實現。

1、goto指令

goto位元組碼指令的生成函數為TemplateTable::_goto(),生成的彙編程式碼如下:(在生成程式碼時添加命令-Xint   -XX:-ProfileInterpreter,這樣可排除生成一些不必要的指令)

// Method*保存到%rcx中
0x00007fffe1019df0: mov    -0x18(%rbp),%rcx
// 將goto後的index(2個位元組)存儲到%edx中
0x00007fffe1019df4: movswl 0x1(%r13),%edx
0x00007fffe1019df9: bswap  %edx
// 算術右移指令
0x00007fffe1019dfb: sar    $0x10,%edx
// 將一個雙字元號擴展後送到一個四字地址中
0x00007fffe1019dfe: movslq %edx,%rdx
// 將當前位元組碼地址加上rdx保存的偏移量,計算跳轉的目標地址
0x00007fffe1019e01: add    %rdx,%r13
// %r13已經變成目標跳轉地址,這裡是載入跳轉地址的第一個位元組碼到rbx中
0x00007fffe1019e04: movzbl 0x0(%r13),%ebx

// continue with the bytecode @ target
// eax: return bci for jsr's, unused otherwise
// ebx: target bytecode
// r13: target bcp
// 開始執行跳轉地址處的位元組碼,其中的常量地址為
// TemplateInterpreter::_active_table的、棧頂快取狀態為vtos的首地址
0x00007fffe1019e09: movabs $0x7ffff73ba4a0,%r10
0x00007fffe1019e13: jmpq   *(%r10,%rbx,8)

其實goto指令實際上生成的彙編程式碼要比上面的程式碼多的多,因為goto指令是一個分支指令,其中會做一些性能統計以輔助進行編譯優化,而且goto如果是在循環中的話,還可能會涉及到棧上替換的技術,所以後面我們在介紹到對應的技術點時再詳細介紹goto指令的其它一些彙編邏輯。 

2、ifeq、ifne等指令  

現在ifeq、ifne等指令的生成函數為TemplateTable::if_0cmp()。ifeq位元組碼指令表示棧頂值與零值比較,當且僅當棧頂的int類型的值為0時,比較結果為真。對應的彙編程式碼如下: 

0x00007fffe10196c7: test   %eax,%eax
// 當棧頂快取%eax不為0時,直接跳到not_taken
0x00007fffe10196c9: jne    0x00007fffe10196f6

// 調用TemplateTable::branch(false,false)函數生成的彙編程式碼

// 將當前棧幀中保存的Method* 拷貝到rcx中
0x00007fffe10196cf: mov    -0x18(%rbp),%rcx
// 將當前位元組碼位置往後偏移1位元組處開始的2位元組數據讀取到edx中
0x00007fffe10196d3: movswl 0x1(%r13),%edx
// 將%edx中的值位元組次序變反
0x00007fffe10196d8: bswap  %edx
// 將edx中的值右移16位,上述兩步就是為了計算跳轉分支的偏移量
0x00007fffe10196da: sar    $0x10,%edx
// 將edx中的數據從2位元組擴展成4位元組
0x00007fffe10196dd: movslq %edx,%rdx
// 將當前位元組碼地址加上rdx保存的偏移量,計算跳轉的目標地址
0x00007fffe10196e0: add    %rdx,%r13
// r13已經變成目標跳轉地址,這裡是載入跳轉地址的第一個位元組碼到ebx中
0x00007fffe10196e3: movzbl 0x0(%r13),%ebx

// 開始執行跳轉地址處的位元組碼,其中的常量地址為
// TemplateInterpreter::_active_table的、棧頂快取狀態為vtos的首地址
0x00007fffe10196e8: movabs $0x7ffff73ba4a0,%r10
0x00007fffe10196f2: jmpq   *(%r10,%rbx,8)

// -- not_taken -- 

類似的指令實現邏輯也高度類似,大家有興趣可自行研究。 

3、lookupswitch、tableswitch等指令 

lookupswitch指令根據鍵值在跳轉表中尋找配對的分支並跳轉,具體的格式如下圖所示。

 

這是一條變長指令並且要求所有的操作數都4位元組對齊,所以緊跟在lookupswitch指令之後可能會有0到3個位元組作為空白填充,而後面的default、npairs等都用4位元組來表示,從當前方法開始(第一條位元組碼指令)計算的地址,即緊隨空白填充的是一系列32位有符號整數值,包括默認跳轉地址default、匹配坐標的數量npairs以及npairs組匹配坐標。其中npairs的值應當大於或等於0,每一組匹配坐標都包含了一個整數值match以及一個有符號32位偏移量offset。上述所有的32位有符號數值都是通過以下方式計算得到:

(byte1<<24)|(byte2<<24)|(byte3<<24)|byte4

tableswitch指令根據鍵值在跳轉表中尋找配對的分支並跳轉,具體的格式如下圖所示。

這是一條變長指令並且要求所有的操作數都4位元組對齊,所以緊跟在lookupswitch指令之後可能會有0到3個位元組作為空白填充,而後面的default、lowbyte、highbyte等用4位元組來表示,從當前方法開始(第一條位元組碼指令)計算的地址,即緊隨空白填充的是一系列32位有符號整數值,包括默認跳轉地址default、高位值high以及低位值low,在此之後是high-low+1個有符號32位偏移offset。上述所有的32位有符號數值都是通過以下方式計算得到:

(byte1<<24)|(byte2<<24)|(byte3<<24)|byte4

生成函數為TemplateTable::tableswitch(),生成的彙編如下:

// align r13,按照4位元組對齊
0x00007fffe1019fa7: lea    0x4(%r13),%rbx
0x00007fffe1019fab: and    $0xfffffffffffffffc,%rbx
// load lo & hi
0x00007fffe1019faf: mov    0x4(%rbx),%ecx
0x00007fffe1019fb2: mov    0x8(%rbx),%edx
0x00007fffe1019fb5: bswap  %ecx
0x00007fffe1019fb7: bswap  %edx

// check against lo & hi
// %ecx中存儲的是lowbyte
0x00007fffe1019fb9: cmp    %ecx,%eax
// 如果比低位值還低,則跳轉到default_case
0x00007fffe1019fbb: jl     0x00007fffe1019feb 
// %edx中存儲的是highbyte
0x00007fffe1019fc1: cmp    %edx,%eax
// 如果比高位值還高,則跳轉到default_case
0x00007fffe1019fc3: jg     0x00007fffe1019feb

// lookup dispatch offset
0x00007fffe1019fc9: sub    %ecx,%eax
// %rbx中存儲的是對齊後的位元組碼指令地址,%rax中存儲的是棧頂快取值
0x00007fffe1019fcb: mov    0xc(%rbx,%rax,4),%edx
// -- continue_execution --
// continue execution
0x00007fffe1019fcf: bswap  %edx
0x00007fffe1019fd1: movslq %edx,%rdx
0x00007fffe1019fd4: movzbl 0x0(%r13,%rdx,1),%ebx
0x00007fffe1019fda: add    %rdx,%r13

0x00007fffe1019fdd: movabs $0x7ffff73ba4a0,%r10
0x00007fffe1019fe7: jmpq   *(%r10,%rbx,8)

// -- default_case --
// handle default
0x00007fffe1019feb: mov (%rbx),%edx 
// 跳轉到continue_execution
0x00007fffe1019fed: jmp 0x00007fffe1019fcf

推薦閱讀:

第1篇-關於JVM運行時,開篇說的簡單些

第2篇-JVM虛擬機這樣來調用Java主類的main()方法

第3篇-CallStub新棧幀的創建

第4篇-JVM終於開始調用Java主類的main()方法啦

第5篇-調用Java方法後彈出棧幀及處理返回結果

第6篇-Java方法新棧幀的創建

第7篇-為Java方法創建棧幀

第8篇-dispatch_next()函數分派位元組碼

第9篇-位元組碼指令的定義

第10篇-初始化模板表

第11篇-認識Stub與StubQueue

第12篇-認識CodeletMark

第13篇-通過InterpreterCodelet存儲機器指令片段

第14篇-生成重要的常式

第15章-解釋器及解釋器生成器

第16章-虛擬機中的彙編器

第17章-x86-64暫存器

第18章-x86指令集之常用指令

第19篇-載入與存儲指令(1)

第20篇-載入與存儲指令之ldc與_fast_aldc指令(2)

第21篇-虛擬機位元組碼之運算指令

第22篇-虛擬機位元組碼指令之類型轉換

第23篇-虛擬機對象操作指令之getstatic

第24篇-虛擬機對象操作指令之getfield

第25篇-虛擬機對象操作指令之putstatic

第26篇-虛擬機位元組碼指令之操作數棧管理指令

如果有問題可直接評論留言或加作者微信mazhimazh

關注公眾號,有HotSpot VM源碼剖析系列文章!