Hotspot JVM の invokestatic と return 命令を読む
前回 Hotspot JVM の frame 構造 について確認したのですが、実際にそれを使用するメソッドの呼出やそこから戻ってくる部分の処理が見れなかったので、invokestatic と return を題材にそのあたりを確認します。
invokestatic
invokestatic 用の native code を作成しているのは以下のメソッドです。
// in src/hotspot/cpu/x86/templateTable_x86.cpp
void TemplateTable::invokestatic(int byte_no) {
prepare_invoke(byte_no, rbx); // get f1 Method*
// do the call
__ profile_call(rax);
__ profile_arguments_type(rax, rbx, rbcp, false);
__ jump_from_interpreted(rbx, rax);
}
profile (method data pointer) まわりを無視すると
- prepare_invoke
- 保存したいレジスタの情報を frame に保存
- レジスタの用意
- return address の用意
- invoke 対象のメソッドの interpret が終わったときの jmp 先になる
-XX:+PrintInterpreter
の invoke return entry points 内のどこか
- jump_from_interpreted
- 現 frame の last sp に rsp をセット
- last sp は frame 作成時には 0x00 であり、このタイミングでセットされている
- jmp
- 現 frame の last sp に rsp をセット
といった処理が行われます。
prepare_invoke
全体を追うのは大変なので invokestatic 関係で大事そうなところのみ抜き出しています。
// in src/hotspot/cpu/x86/templateTable_x86.cpp
void TemplateTable::prepare_invoke(int byte_no,
Register method, // linked method (or i-klass)
Register index, // itable index, MethodType, etc.
Register recv, // if caller wants to see it
Register flags // if caller wants to test it
) {
...
if (flags == noreg) flags = rdx;
...
// save 'interpreter return address'
__ save_bcp();
load_invoke_cp_cache_entry(byte_no, method, index, flags, is_invokevirtual, false, is_invokedynamic);
...
// compute return type
__ shrl(flags, ConstantPoolCacheEntry::tos_state_shift);
// Make sure we don't need to mask fjlags after the above shift
ConstantPoolCacheEntry::verify_tos_state_shift();
// load return address
{
const address table_addr = (address) Interpreter::invoke_return_entry_table_for(code);
ExternalAddress table(table_addr);
LP64_ONLY(__ lea(rscratch1, table));
LP64_ONLY(__ movptr(flags, Address(rscratch1, flags, Address::times_ptr)));
}
// push return address
__ push(flags);
...
}
以下のような処理が行われているようです。
- save_bcp
- bcp register (x86_64 の場合 r13) を frame 内の bcp 領域に退避
- interpret 中はレジスタしか更新していないのでここで最新の値を反映する
- load_invoke_cp_cache_entry
- method register (x86_64 の場合 rbx) に invoke 対象の
Method*
を設定
- method register (x86_64 の場合 rbx) に invoke 対象の
- compute return type
- flags register から tos にあたる情報を抜き出し
- load return address
- push return address
- この 2 つで invoke したメソッド終了時の戻り先を計算しスタックにプッシュしている
- 戻り先は専用のテーブルで管理され、invoke 命令の種類 (e.g. invokestatic) や tos 等によって決まる
jump_from_interpreted
void InterpreterMacroAssembler::prepare_to_jump_from_interpreted() {
// set sender sp
lea(_bcp_register, Address(rsp, wordSize));
// record last_sp
movptr(Address(rbp, frame::interpreter_frame_last_sp_offset * wordSize), _bcp_register);
}
// Jump to from_interpreted entry of a call unless single stepping is possible
// in this thread in which case we must call the i2i entry
void InterpreterMacroAssembler::jump_from_interpreted(Register method, Register temp) {
prepare_to_jump_from_interpreted();
...
jmp(Address(method, Method::from_interpreted_offset()));
}
- Method::from_interpreted_offset に入っているのは例えば TemplateInterpreterGenerator の generate_normal_entry で生成されたエントリポイントのアドレス
- ここで新たな frame が用意されてからメソッドの interpret をする
実際にここまでの挙動を gdb で確認してみます。 例として以下のような Java プログラムを利用します。
class InvokeStatic {
public static void f(int a, int b) {
int sum = a + b;
System.out.println("sum: " + sum);
}
public static void main(String[] args) {
int x = 1;
int y = 2;
f(1 + 2, 2 + 2);
}
}
$ javap -c -v InvokeStatic.class
...
InvokeStatic();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void f(int, int);
descriptor: (II)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=2
0: iload_0
1: iload_1
2: iadd
3: istore_2
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: new #3 // class java/lang/StringBuilder
10: dup
11: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
14: ldc #5 // String sum:
16: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: iload_2
20: invokevirtual #7 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
23: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
26: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
29: return
LineNumberTable:
line 3: 0
line 4: 4
line 5: 29
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iconst_3
5: iconst_4
6: invokestatic #10 // Method f:(II)V
9: return
LineNumberTable:
line 8: 0
line 9: 2
line 10: 4
line 11: 9
...
main メソッドから f メソッドを呼び出す invokestatic 実行時 (invokestatic の native code 開始時) のスタックの様子は以下のようになります。main メソッドの frame が確認できています (sp = stack pointer, fp = frame pointer, bcp = byte code pointer)。
# breakpoint at the beginning of invokestatic
(gdb) p $rbp
$1 = (void *) 0x7ffff59ed8d8
(gdb) p $rsp
$2 = (void *) 0x7ffff59ed880
(gdb) p ($rbp - $rsp)
$3 = 88
(gdb) x /128xb $rsp
0x7ffff59ed880: 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 # operand stack <- rsp
0x7ffff59ed888: 0x03 0x00 0x00 0x00 0x00 0x00 0x00 0x00 # operand stack
0x7ffff59ed890: 0x90 0xd8 0x9e 0xf5 0xff 0x7f 0x00 0x00 # expression stack
0x7ffff59ed898: 0x48 0x54 0xa2 0xcd 0xff 0x7f 0x00 0x00 # bcp
0x7ffff59ed8a0: 0xf8 0xd8 0x9e 0xf5 0xff 0x7f 0x00 0x00 # pointer to locals
0x7ffff59ed8a8: 0xc8 0x54 0xa2 0xcd 0xff 0x7f 0x00 0x00 # constant pool cache
0x7ffff59ed8b0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 # methodData
0x7ffff59ed8b8: 0x18 0x54 0x6f 0x19 0x07 0x00 0x00 0x00 # mirror
0x7ffff59ed8c0: 0x60 0x54 0xa2 0xcd 0xff 0x7f 0x00 0x00 # Method*
0x7ffff59ed8c8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 # last sp
0x7ffff59ed8d0: 0xf8 0xd8 0x9e 0xf5 0xff 0x7f 0x00 0x00 # old sp 1
0x7ffff59ed8d8: 0x60 0xd9 0x9e 0xf5 0xff 0x7f 0x00 0x00 # old fp 1 <- rbp
0x7ffff59ed8e0: 0xf3 0x09 0x00 0xe1 0xff 0x7f 0x00 0x00 # return address 1
0x7ffff59ed8e8: 0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x00 # local
0x7ffff59ed8f0: 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 # local
0x7ffff59ed8f8: 0x60 0x59 0x6f 0x19 0x07 0x00 0x00 0x00 # param
以下は normal_entry へ jmp する直前のスタックの様子です。
# breakpoint at jmp (to method entrypoint) of invokestatic
(gdb) p $rbp
$4 = (void *) 0x7ffff59ed8d8
(gdb) p $rsp
$5 = (void *) 0x7ffff59ed878
(gdb) x /136xb $rsp
0x7ffff59ed878: 0xa7 0x9f 0x00 0xe1 0xff 0x7f 0x00 0x00 # return address 2 <- rsp
0x7ffff59ed880: 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 # operand stack <- r13
0x7ffff59ed888: 0x03 0x00 0x00 0x00 0x00 0x00 0x00 0x00 # operand stack
0x7ffff59ed890: 0x90 0xd8 0x9e 0xf5 0xff 0x7f 0x00 0x00
0x7ffff59ed898: 0x4e 0x54 0xa2 0xcd 0xff 0x7f 0x00 0x00
0x7ffff59ed8a0: 0xf8 0xd8 0x9e 0xf5 0xff 0x7f 0x00 0x00
0x7ffff59ed8a8: 0xc8 0x54 0xa2 0xcd 0xff 0x7f 0x00 0x00
0x7ffff59ed8b0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7ffff59ed8b8: 0x18 0x54 0x6f 0x19 0x07 0x00 0x00 0x00
0x7ffff59ed8c0: 0x60 0x54 0xa2 0xcd 0xff 0x7f 0x00 0x00
0x7ffff59ed8c8: 0x80 0xd8 0x9e 0xf5 0xff 0x7f 0x00 0x00 # last sp
0x7ffff59ed8d0: 0xf8 0xd8 0x9e 0xf5 0xff 0x7f 0x00 0x00 # old sp 1
0x7ffff59ed8d8: 0x60 0xd9 0x9e 0xf5 0xff 0x7f 0x00 0x00 # old fp 1 <- rbp
0x7ffff59ed8e0: 0xf3 0x09 0x00 0xe1 0xff 0x7f 0x00 0x00 # return address 1
0x7ffff59ed8e8: 0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7ffff59ed8f0: 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7ffff59ed8f8: 0x60 0x59 0x6f 0x19 0x07 0x00 0x00 0x00
- last sp に rsp が設定されている
- この時点の rsp + word のアドレスを指す
- 同じ値が r13 レジスタにも入っている
- 計算した return address 2 がスタックに push されている
このあと normal_entry で新しい frame が作成され、f メソッドの interpret を開始した時点のスタックは以下のようになります。図でメモリアドレス 0x7ffff59ed820 - 0x7ffff59ed878 のあたりが新 frame 部分です。
# breakpoint at the beginning of interpretation of method 'f'
(gdb) p $rbp
$6 = (void *) 0x7ffff59ed868
(gdb) p $rsp
$7 = (void *) 0x7ffff59ed820
(gdb) p (((long) 0x7ffff59ed8f8) - ((long) $rsp))
$9 = 216
(gdb) x /216xb $rsp
(以下は f メソッドの frame)
0x7ffff59ed820: 0x20 0xd8 0x9e 0xf5 0xff 0x7f 0x00 0x00 # <- rsp
0x7ffff59ed828: 0x80 0x53 0xa2 0xcd 0xff 0x7f 0x00 0x00
0x7ffff59ed830: 0x88 0xd8 0x9e 0xf5 0xff 0x7f 0x00 0x00
0x7ffff59ed838: 0xc8 0x54 0xa2 0xcd 0xff 0x7f 0x00 0x00
0x7ffff59ed840: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7ffff59ed848: 0x18 0x54 0x6f 0x19 0x07 0x00 0x00 0x00
0x7ffff59ed850: 0xa8 0x53 0xa2 0xcd 0xff 0x7f 0x00 0x00
0x7ffff59ed858: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7ffff59ed860: 0x80 0xd8 0x9e 0xf5 0xff 0x7f 0x00 0x00 # old sp2
0x7ffff59ed868: 0xd8 0xd8 0x9e 0xf5 0xff 0x7f 0x00 0x00 # old fp2 <- rbp
0x7ffff59ed870: 0xa7 0x9f 0x00 0xe1 0xff 0x7f 0x00 0x00 # return address 2
0x7ffff59ed878: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 # local
(以下は main メソッドの frame)
0x7ffff59ed880: 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 # param (<- old sp2)
0x7ffff59ed888: 0x03 0x00 0x00 0x00 0x00 0x00 0x00 0x00 # param
0x7ffff59ed890: 0x90 0xd8 0x9e 0xf5 0xff 0x7f 0x00 0x00
0x7ffff59ed898: 0x4e 0x54 0xa2 0xcd 0xff 0x7f 0x00 0x00
0x7ffff59ed8a0: 0xf8 0xd8 0x9e 0xf5 0xff 0x7f 0x00 0x00
0x7ffff59ed8a8: 0xc8 0x54 0xa2 0xcd 0xff 0x7f 0x00 0x00
0x7ffff59ed8b0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7ffff59ed8b8: 0x18 0x54 0x6f 0x19 0x07 0x00 0x00 0x00
0x7ffff59ed8c0: 0x60 0x54 0xa2 0xcd 0xff 0x7f 0x00 0x00
0x7ffff59ed8c8: 0x80 0xd8 0x9e 0xf5 0xff 0x7f 0x00 0x00
0x7ffff59ed8d0: 0xf8 0xd8 0x9e 0xf5 0xff 0x7f 0x00 0x00
0x7ffff59ed8d8: 0x60 0xd9 0x9e 0xf5 0xff 0x7f 0x00 0x00 # (<- old fp2)
0x7ffff59ed8e0: 0xf3 0x09 0x00 0xe1 0xff 0x7f 0x00 0x00
0x7ffff59ed8e8: 0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7ffff59ed8f0: 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
return
return 用の native code を作成しているのは以下のメソッドです。
どうやら bytecode が _return_register_finalizer
である場合は追加の処理がありますがここでは無視しています。
// in src/hotspot/cpu/x86/templateTable_x86.cpp
void TemplateTable::_return(TosState state) {
...
// Narrow result if state is itos but result type is smaller.
// Need to narrow in the return bytecode rather than in generate_return_entry
// since compiled code callers expect the result to already be narrowed.
if (state == itos) {
__ narrow(rax);
}
__ remove_activation(state, rbcp);
__ jmp(rbcp);
}
- メソッド f 用の frame を解放する
- rbp を メソッド main の interpret 時のものに戻す (old fp2)
- rsp を old sp2 に戻す
といったことが行われます。
そのあと、rbcp (r13) レジスタに invokestatic でセットした return address 2 がセットされるのでそこへ jmp します。
# breakpoint at the return address 2 (after jmp)
(gdb) p $rbp
$203 = (void *) 0x7ffff59ed8d8
(gdb) p $rsp
$204 = (void *) 0x7ffff59ed880
(gdb) x /128xb $rsp
(main メソッドの frame)
0x7ffff59ed880: 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 # <- rsp
0x7ffff59ed888: 0x03 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7ffff59ed890: 0x90 0xd8 0x9e 0xf5 0xff 0x7f 0x00 0x00
0x7ffff59ed898: 0x4e 0x54 0xa2 0xcd 0xff 0x7f 0x00 0x00
0x7ffff59ed8a0: 0xf8 0xd8 0x9e 0xf5 0xff 0x7f 0x00 0x00
0x7ffff59ed8a8: 0xc8 0x54 0xa2 0xcd 0xff 0x7f 0x00 0x00
0x7ffff59ed8b0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7ffff59ed8b8: 0x18 0x54 0x6f 0x19 0x07 0x00 0x00 0x00
0x7ffff59ed8c0: 0x60 0x54 0xa2 0xcd 0xff 0x7f 0x00 0x00
0x7ffff59ed8c8: 0x80 0xd8 0x9e 0xf5 0xff 0x7f 0x00 0x00
0x7ffff59ed8d0: 0xf8 0xd8 0x9e 0xf5 0xff 0x7f 0x00 0x00
0x7ffff59ed8d8: 0x60 0xd9 0x9e 0xf5 0xff 0x7f 0x00 0x00 # <- rbp
0x7ffff59ed8e0: 0xf3 0x09 0x00 0xe1 0xff 0x7f 0x00 0x00
0x7ffff59ed8e8: 0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7ffff59ed8f0: 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7ffff59ed8f8: 0x60 0x59 0x6f 0x19 0x07 0x00 0x00 0x00
jmp 後の return entry points 内の処理では frame からレジスタに値を戻したり (e.g. bcp_registers, local_registers) してから dispath_next で main 内 invokestatic の次の Java byte code から interpret を再開します。
まとめると
invokestatic (Java byte code)
↓
normal entry
↓
Java byte codes in the invoked method
↓
return (Java byte code)
↓
return entry points
↓
continue to interpret of the caller (who executed invokestatic) method
という感じでした。