Hotspot JVM の Frame について
Hotspot JVM 内 interpret 処理時に使用される frame についてまとめました。
記事内容は
- OpenJDK バージョン: jdk11u の changeset 51892:e86c5c20e188
- OS: Linux distribution の何か
- CPU architecture: x86_64
という環境の想定で書いています。
frame について
Hotspot JVM の Template Interpreter には frame というクラスが定義されています。これは Java メソッド処理時に使用されるスタックフレームを表現するクラスであり、例えばローカル変数、メソッドパラメータ、リターンアドレスといったものが含まれます。各 Java メソッドの interpret 開始時に用意され、終了時に破棄されます。
frame の実態は CPU 依存であり、x86 では以下のような構造です (src/hotspot/cpu/x86/frame_x86.hpp
のコメントより引用。図では上が低アドレス)。
// ------------------------------ Asm interpreter ----------------------------------------
// Layout of asm interpreter frame:
// [expression stack ] * <- sp
// [monitors ] \
// ... | monitor block size
// [monitors ] /
// [monitor block size ]
// [byte code pointer ] = bcp() bcp_offset
// [pointer to locals ] = locals() locals_offset
// [constant pool cache ] = cache() cache_offset
// [methodData ] = mdp() mdx_offset
// [Method* ] = method() method_offset
// [last sp ] = last_sp() last_sp_offset
// [old stack pointer ] (sender_sp) sender_sp_offset
// [old frame pointer ] <- fp = link()
// [return pc ]
// [oop temp ] (only for native calls)
// [locals and parameters ]
// <- sender sp
// ------------------------------ Asm interpreter ----------------------------------------
frame の各要素:
- expression stack
- frame 作成完了時の rsp? (= 自分自身のメモリアドレス)
- monitors
- わからない。多分モニタロック関連?
- monitor block size
- 多分上の monitors の数
- byte code pointer
- 次に interpret する Java byte code アドレスを指す
- bcp と略されることも
- pointer to locals
- スタック上の parameters 開始アドレス
- constant pool cache
- わからない。多分 constant pool の情報をキャッシュする仕組みがあってそれ関連
- cache は interpreter から VM runtime に戻る回数を減らすためのはず
- わからない。多分 constant pool の情報をキャッシュする仕組みがあってそれ関連
- methodData
- わからない。メソッドの統計情報を集めて JIT コンパイル等の最適化に利用する?
- Method*
- interpret 対象のメソッドへのポインタ
- last sp
- frame 作成時は 0x00
- old stack pointer
- frame 作成前の rsp
- sender sp とも呼ばれる
- old frame pointer
- frame 作成前の rbp
- return pc
- interpret 完了時の戻り先
- (Java byte code ではなく native code 上のアドレス)
- oop temp
- わからない
- locals and parameters
- メソッドのローカル変数やパラメータの置かれる領域
frame の利用用途は上に書いた通り Java メソッド interpret 時のスタックフレームですが、 frame_x86.hpp
のコメントでは
A frame represents a physical stack frame (an activation). Frames can be C or Java frames, and the Java frames can be interpreted or compiled
と説明されており、それ以外にも用途があるのかなと思っています。C frame, Java frame の違いがわからないと難しいです。
また Hotspot JVM の interpreter にはここで取り上げている Template Interpreter 以外に Cpp Interpreter というのがあるのですが (前回のポスト)、そちらではこの frame は使わないようです。RuntimeOverview#interpreter の記述に依れば Cpp Interpreter では native stack とは別にスタックフレームを管理する (なのでオーバーヘッドがある) とのことです。
it (CppInterpreter) uses a separate software stack to pass Java arguments, while the native C stack is used by the VM itself. A number of JVM internal variables, such as the program counter or the stack pointer for a Java thread, are stored in C variables, which are not guaranteed to be always kept in the hardware registers. Management of these software interpreter structures consumes a considerable share of total execution time.
main 開始処理のソースリーディング
frame 使用例として Hotspot JVM が Java プログラムのエントリポイントである public static void main
の interpret をどのように開始するかを見ていきます。
main メソッドの interpret を開始するのは JavaCalls::call
です。このメソッドは VM runtime から Java byte code の interpret へと移行する際に使用されると認識しています。
VM runtime と interpreter の関係というのは OS とユーザプログラムの関係に似ていると感じます。(例えば main メソッド実行のために) interpret を開始するときは VM runtime が用意をしてから interpreter に処理を開始させますが、これは OS がユーザプログラムをメモリにロードしたりするのに似ていますし、interpreter では困難な処理 (e.g. constant pool の参照) が必要なときに一度 VM runtime に処理をお願いしてまた戻ってくる、といった処理はシステムコールに似ています。
JavaCalls::call
での VM runtime から interpreter への移行ステップを把握するために、ここでは以下の 2 つに分けて見ていきます。
- (1) interpret 前の状態を保存 (generated by
generate_call_stub
) - (2) frame の作成 (generated by
generate_normal_entry
)
この 2 つの処理はどちらも VM 初期化時に生成した native code で処理が行われます。以降では便宜上この native code をそれぞれ call_stub, normal_entry コードと呼びます。JavaCalls::call
を読むと、以下の StubRoutines::call_stub
で call_stub の処理を実行し、その内部で entry_point
として渡した normal_entry コードを実行、そこから Java メソッドの interpret を始める、というようになります。
// in src/hotspot/share/runtime/javaCalls.cpp
void JavaCalls::call_helper(JavaValue* result, const methodHandle& method, JavaCallArguments* args, TRAPS) {
...
StubRoutines::call_stub()(
(address)&link,
// (intptr_t*)&(result->_value), // see NOTE above (compiler problem)
result_val_address, // see NOTE above (compiler problem)
result_type,
method(),
entry_point,
args->parameters(),
args->size_of_parameters(),
CHECK
);
...
}
順番が前後しますが、まず entry_point
の正体を追いながら normal_entry で frame が作成される様子を確認し、そのあと call_stub について同様にコード生成部分とその内容を確認していきます。
entry_point の正体
JavaCalls::call_helper
では以下のように interpreter 用のエントリポイントを持ってきています。
// in JavaCalls::call_helper
// Since the call stub sets up like the interpreter we call the from_interpreted_entry
// so we can go compiled via a i2c. Otherwise initial entry method will always
// run interpreted.
address entry_point = method->from_interpreted_entry();
if (JvmtiExport::can_post_interpreter_events() && thread->is_interp_only_mode()) {
entry_point = method->interpreter_entry();
}
Method はエントリポイントのアドレスを複数種類持っています。これは恐らく interpreter 用であったり、JIT コンパイルされたコード用だったりするのだと思います。
ここでは interpreter の処理を見たいので method->interpreter_entry
に着目します。これは単に Method の持つ address _i2i_entry
を持ってきているだけです。
// in method.hpp
// Entry point for calling both from and to the interpreter.
address _i2i_entry; // All-args-on-stack calling convention
これがどのように生成されているかを見るとどうやらMethod::link_method
のようです。link というのは JVM spec 11 の 5.4 Linking で触れられる操作を指しているのかと思います。
// in method.hpp and method.cpp
// setup entry points
//
// Called when the method_holder is getting linked. Setup entrypoints so the method
// is ready to be called from interpreter, compiler, and vtables.
void link_method(const methodHandle& method, TRAPS) {
...
if (!is_shared()) {
assert(adapter() == NULL, "init'd to NULL");
address entry = Interpreter::entry_for_method(h_method);
assert(entry != NULL, "interpreter entry must be non-null");
// Sets both _i2i_entry and _from_interpreted_entry
set_interpreter_entry(entry);
}
...
}
set_interpreter_entry
は i2i_entry
等への設定を行っているだけなので、大事なのは Interpreter::entry_for_method(h_method)
から返されるエントリポイントアドレスです。
// in abstractInterpreter.hpp
static address entry_for_method(const methodHandle& m) {
return entry_for_kind(method_kind(m));
}
static address entry_for_kind(MethodKind k) {
assert(0 <= k && k < number_of_method_entries, "illegal kind");
return _entry_table[k];
}
Interpreter::_entry_table
から返すアドレスを求めていることがわかります。MethodKind
というのは enum 型で以下のような値が定義されます。いま見ている main メソッドに関していえば zerolocals
でいいはずです。
enum MethodKind {
zerolocals, // method needs locals initialization
zerolocals_synchronized, // method needs locals initialization & is synchronized
native, // native method
native_synchronized, // native method & is synchronized
empty, // empty method (code: _return)
accessor, // accessor method (code: _aload_0, _getfield, _(a|i)return)
abstract, // abstract method (throws an AbstractMethodException)
method_handle_invoke_FIRST, // java.lang.invoke.MethodHandles::invokeExact, etc.
method_handle_invoke_LAST = (method_handle_invoke_FIRST
+ (vmIntrinsics::LAST_MH_SIG_POLY
- vmIntrinsics::FIRST_MH_SIG_POLY)),
java_lang_math_sin, // implementation of java.lang.Math.sin (x)
java_lang_math_cos, // implementation of java.lang.Math.cos (x)
java_lang_math_tan, // implementation of java.lang.Math.tan (x)
java_lang_math_abs, // implementation of java.lang.Math.abs (x)
java_lang_math_sqrt, // implementation of java.lang.Math.sqrt (x)
java_lang_math_log, // implementation of java.lang.Math.log (x)
java_lang_math_log10, // implementation of java.lang.Math.log10 (x)
java_lang_math_pow, // implementation of java.lang.Math.pow (x,y)
java_lang_math_exp, // implementation of java.lang.Math.exp (x)
java_lang_math_fmaF, // implementation of java.lang.Math.fma (x, y, z)
java_lang_math_fmaD, // implementation of java.lang.Math.fma (x, y, z)
java_lang_ref_reference_get, // implementation of java.lang.ref.Reference.get()
java_util_zip_CRC32_update, // implementation of java.util.zip.CRC32.update()
java_util_zip_CRC32_updateBytes, // implementation of java.util.zip.CRC32.updateBytes()
java_util_zip_CRC32_updateByteBuffer, // implementation of java.util.zip.CRC32.updateByteBuffer()
java_util_zip_CRC32C_updateBytes, // implementation of java.util.zip.CRC32C.updateBytes(crc, b[], off, end)
java_util_zip_CRC32C_updateDirectByteBuffer, // implementation of java.util.zip.CRC32C.updateDirectByteBuffer(crc, address, off, end)
java_lang_Float_intBitsToFloat, // implementation of java.lang.Float.intBitsToFloat()
java_lang_Float_floatToRawIntBits, // implementation of java.lang.Float.floatToRawIntBits()
java_lang_Double_longBitsToDouble, // implementation of java.lang.Double.longBitsToDouble()
java_lang_Double_doubleToRawLongBits, // implementation of java.lang.Double.doubleToRawLongBits()
number_of_method_entries,
invalid = -1
};
Interpreter::_entry_table
の初期化は VM の初期化の一部として TemplateInterpreterGenerator::generate_all
で行われます。
// in src/hotspot/cpu/x86/templateInterpreterGenerator.cpp
void TemplateInterpreterGenerator::generate_all() {
...
#define method_entry(kind) \
{ CodeletMark cm(_masm, "method entry point (kind = " #kind ")"); \
Interpreter::_entry_table[Interpreter::kind] = generate_method_entry(Interpreter::kind); \
Interpreter::update_cds_entry_table(Interpreter::kind); \
}
// all non-native method kinds
method_entry(zerolocals)
...
}
generate_method_entry
を見ると zerolocals については generate_normal_entry
でコード生成しています。
// in src/hotspot/cpu/x86/templateInterpreterGenerator_x86.cpp
address TemplateInterpreterGenerator::generate_normal_entry(bool synchronized) {
// determine code generation flags
bool inc_counter = UseCompiler || CountCompiledCalls || LogTouchedMethods;
// ebx: Method*
// rbcp: sender sp
address entry_point = __ pc();
const Address constMethod(rbx, Method::const_offset());
const Address access_flags(rbx, Method::access_flags_offset());
const Address size_of_parameters(rdx,
ConstMethod::size_of_parameters_offset());
const Address size_of_locals(rdx, ConstMethod::size_of_locals_offset());
// 以下 assembler を使用したコード生成がずらずら
...
}
ということで generate_normal_entry が目的の frame 作成を行う native code 生成部分になります。
なお generate_normal_entry で生成される native code は -XX:+PrintInterpreter
で確認できます。
method entry point (kind = zerolocals) [0x00007f2bd5016140, 0x00007f2bd5016f20] 3552 bytes
0x00007f2bd5016140: mov 0x10(%rbx),%rdx
0x00007f2bd5016144: movzwl 0x34(%rdx),%ecx
0x00007f2bd5016148: movzwl 0x32(%rdx),%edx
...
normal_entry コード内容の確認
ここでは前節で追いかけた generate_normal_entry が生成するコード (normal_entry) の内容を大まかに把握していきます。
normal_entry 開始時のレジスタやスタックの様子は以下のようになります。レジスタについては generate_normal_entry メソッドはじめのコメントから、スタックについては後述の call_stub コードから判断しています。
![Stack and registers at the beginning of generate_normal_entry Stack and registers at the beginning of generate_normal_entry](/images/openjdk-frame/generate-normal-entry1.jpg)
- rbp レジスタの指す先には old rbp
- call_stub 呼び出し時の rbp
- param1-n は call_stub 内で設定済
- (この時点で) r13 レジスタの指す先は sender sp と呼ばれている
- normal_entry 呼び出し時の stack pointer という感じ?
- rsp の指す先は normal_entry 呼び出し時の call 命令で設定した return address
- ebx レジスタに interpret 対象の Java メソッドへの参照 (
Method*
)
実際に gdb で normal_entry 開始時のスタック、レジスタの様子を確認してみます。サンプルとして以下の Java プログラムを使用します。
class Locals {
public static void main(String[] args) {
int x = 1;
int y = 2;
int z = 3;
int sum = x + y + z;
System.out.println("sum: " + sum);
}
}
$ java Locals.java
$ javap -c -v Locals.class
...
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=5, args_size=1
...
# breakpoint at the beginning of method entry point (kind=zerolocals)
(gdb) p $rsp
$3 = (void *) 0x7ffff59ed8f0
(gdb) p $rbp
$4 = (void *) 0x7ffff59ed960
(gdb) p ($rbp - $rsp) / 8
$7 = 14
(gdb) x /120xb $rsp
0x7ffff59ed8f0: 0xf3 0x09 0x00 0xe1 0xff 0x7f 0x00 0x00 # <- rsp
0x7ffff59ed8f8: 0x38 0x57 0x6f 0x19 0x07 0x00 0x00 0x00 # param1 (args) <- r13
0x7ffff59ed900: 0xa0 0x1f 0x00 0x00 0xff 0x7f 0x00 0x00
0x7ffff59ed908: 0x40 0xda 0x9e 0xf5 0xff 0x7f 0x00 0x00
0x7ffff59ed910: 0x00 0xdc 0x9e 0xf5 0xff 0x7f 0x00 0x00
0x7ffff59ed918: 0x40 0xdb 0x9e 0xf5 0xff 0x7f 0x00 0x00
0x7ffff59ed920: 0x00 0xa8 0x01 0xf0 0xff 0x7f 0x00 0x00
0x7ffff59ed928: 0xe0 0xdc 0x9e 0xf5 0xff 0x7f 0x00 0x00
0x7ffff59ed930: 0x40 0xda 0x9e 0xf5 0xff 0x7f 0x00 0x00
0x7ffff59ed938: 0xe8 0xdc 0x9e 0xf5 0xff 0x7f 0x00 0x00
0x7ffff59ed940: 0x0a 0x00 0x00 0x00 0xff 0x7f 0x00 0x00
0x7ffff59ed948: 0x88 0x53 0xa2 0xcd 0xff 0x7f 0x00 0x00
0x7ffff59ed950: 0x40 0x61 0x01 0xe1 0xff 0x7f 0x00 0x00
0x7ffff59ed958: 0x10 0xdc 0x9e 0xf5 0xff 0x7f 0x00 0x00
0x7ffff59ed960: 0xc0 0xda 0x9e 0xf5 0xff 0x7f 0x00 0x00 # <- rbp
# check param1
(gdb) x /40xb 0x07196f5738
0x7196f5738: 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7196f5740: 0xa0 0x5c 0x02 0x00 0x02 0x00 0x00 0x00
0x7196f5748: 0xed 0xea 0x2d 0xe3 0xf6 0xea 0x2d 0xe3
0x7196f5750: 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7196f5758: 0x40 0x08 0x00 0x00 0x03 0x00 0x00 0x00
(gdb) p /x $r13
$6 = 0x7ffff59ed8f8
(gdb) p ((Method *) $rbx)->_constMethod->_constants->_pool_holder->_name->as_C_string()
$1 = 0x7ffff00193d0 "Locals"
はじめの rbp, rsp, その間のスタック領域の出力は図と一致した結果が得られています。(rbp-1) から (rbp-12) までの 12 word は call_stub 内で用意されています。
param1 の指す先を見ると、配列オブジェクトっぽい構造が見られます。
r13 レジスタも想定通りの場所を指しています。
rbx レジスタの内容からメソッドを所有するクラス名を辿ることができています。想定通り Method *
の値がセットされていると見てよさそうです。
1. locals の用意
まずは locals (ローカル変数) 領域をスタックに確保します。
必要なサイズは max locals - parameters - receiver です (ただし receiver は static method であれば存在しない)。これらは javap で見たときにメソッドの情報の一部として確認できます。上の Locals クラスの例でいえば、max locals は 5, parameters は 1 なので必要な locals は 4 となります。
normal_entry 内ではこれらの情報を ebx レジスタの指す Method*
から計算します。
![Prepare locals Prepate locals](/images/openjdk-frame/generate-normal-entry2.jpg)
- r14 レジスタはスタック上の parameters 開始位置を指す
- locals を入れる前に rax レジスタに return address を pop する
2. frame の作成
locals 以降の frame の値を設定します。
まずは退避していた rax レジスタの return address をスタックに戻し、rbp の値を rsp と同じ値に更新します。図のように rsp, rbp の指す先は以前の rbp となっており、frame 内では old frame pointer と呼びます。
![push(rax) and enter() push(rax) and enter()](/images/openjdk-frame/generate-normal-entry3.jpg)
残りの frame を設定します。あまり理解のできていない要素もいくつかありますが図にまとめました。
![Prepare frame values Prepare frame values](/images/openjdk-frame/generate-normal-entry4.jpg)
- r13 レジスタはここで 2 度異なる用途で使用している
- まずはじめに格納していた sender sp を push する (frame 内 old stack pointer と呼ぶ)
- 次に interpret 対象 Method の開始 byte code へのアドレスを計算し、それを push する (frame 内 bytecode pointer と呼ぶ)
- last sp はこの時点では 0x00
- 確か内部でまた別のメソッド呼ぶときにその時点の rsp を設定するみたいだった気が?
- mirror, methodData, constant pool cache は理解不足
- あと mirror が
frame_x86.hpp
のコメントには存在しないんだけど、多分コメントが間違っている。アセンブリにはどう考えても存在するし、他の CPU 実装では存在する
- あと mirror が
この時点の frame の様子を gdb で見てみます。上と同様に Locals クラスを使用しています。
# breakpoint after finishing generate frame
# set breakpoint judging by TemplateInterpreterGenerator::generate_fixed_frame
(gdb) p $rsp
$13 = (void *) 0x7ffff59ed880
(gdb) p $rbp
$14 = (void *) 0x7ffff59ed8c8
(gdb) p ($rbp - $rsp) / 8
$15 = 9
(gdb) x /160xb $rsp
0x7ffff59ed880: 0x80 0xd8 0x9e 0xf5 0xff 0x7f 0x00 0x00 # <- rsp
0x7ffff59ed888: 0x58 0x53 0xa2 0xcd 0xff 0x7f 0x00 0x00 # bytecode pointer
0x7ffff59ed890: 0xf8 0xd8 0x9e 0xf5 0xff 0x7f 0x00 0x00 # pointer to locals
0x7ffff59ed898: 0xf0 0x53 0xa2 0xcd 0xff 0x7f 0x00 0x00 # constant pool cache
0x7ffff59ed8a0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 # methodData
0x7ffff59ed8a8: 0x98 0x52 0x6f 0x19 0x07 0x00 0x00 0x00 # mirror
0x7ffff59ed8b0: 0x88 0x53 0xa2 0xcd 0xff 0x7f 0x00 0x00 # Method*
0x7ffff59ed8b8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 # last sp
0x7ffff59ed8c0: 0xf8 0xd8 0x9e 0xf5 0xff 0x7f 0x00 0x00 # old stack pointer
0x7ffff59ed8c8: 0x60 0xd9 0x9e 0xf5 0xff 0x7f 0x00 0x00 # old frame pointer <- rbp
0x7ffff59ed8d0: 0xf3 0x09 0x00 0xe1 0xff 0x7f 0x00 0x00 # return address
0x7ffff59ed8d8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 # local 4
0x7ffff59ed8e0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 # local 3
0x7ffff59ed8e8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 # local 2
0x7ffff59ed8f0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 # local 1
0x7ffff59ed8f8: 0x38 0x57 0x6f 0x19 0x07 0x00 0x00 0x00 # param 1
0x7ffff59ed900: 0xa0 0x1f 0x00 0x00 0xff 0x7f 0x00 0x00
0x7ffff59ed908: 0x40 0xda 0x9e 0xf5 0xff 0x7f 0x00 0x00
0x7ffff59ed910: 0x00 0xdc 0x9e 0xf5 0xff 0x7f 0x00 0x00
0x7ffff59ed918: 0x40 0xdb 0x9e 0xf5 0xff 0x7f 0x00 0x00
ざっとですがこれで frame の設定は完了です。
3. dispatch_next
normal_entry では frame 作成後 interpret を開始するための処理が続きます。これは InterpreterMacroAssembler::dispatch_next
で生成されるコードによる処理の部分で、あまり追えていませんが
- rscratch1 (r10) レジスタに dispatch_table のアドレスをセット
- rbcp (r13) レジスタと rscratch1 (r10) レジスタから最初の Java byte code 用の interpret エントリポイントへ jmp
という感じで interpret が開始されるようです。
StubRoutines::call_stub の正体
normal_entry での frame 作成処理について確認したので、次に call_stub を見ていきます。
JavaCalls::call
では StubRoutines::call_stub()
で call_stub 呼び出しをしていました。
// in src/hotspot/share/runtime/stubRoutines.hpp
class StubRoutines: AllStatic {
...
static CallStub call_stub() {
return CAST_TO_FN_PTR(CallStub, _call_stub_entry);
}
...
}
_call_stub_entry
は stubGenerator で定義される generate_call_stub で生成されます。
generate_call_stub は StubGenerator のコンストラクタから呼ばれる generate_initial で呼ばれます。
// in src/hotspot/cpu/x86/stubGeneratro_x86_64.cpp
class StubGenerator: public StubCodeGenerator {
...
address generate_call_stub(address& return_address) {
...
}
...
// Initialization
void generate_initial() {
// Generates all stubs and initializes the entry points
...
StubRoutines::_call_stub_entry =
generate_call_stub(StubRoutines::_call_stub_return_address);
...
}
...
}
ということで call_stub コードは stubGenerator の generate_call_stub によって生成されます。
実際に生成されるコードは -XX:+PrintStubCode
により確認できます。
StubRoutines::call_stub [0x00007f1ddd0008e4, 0x00007f1ddd000c22[ (830 bytes)
0x00007f1ddd0008e4: push %rbp
0x00007f1ddd0008e5: mov %rsp,%rbp
0x00007f1ddd0008e8: sub $0x60,%rsp
...
call_stub コード内容の確認
call_stub の内容を大まかに把握していきます。
1. enter() で rbp の保存
まずは push rbp
からの mov rsp, rbp
で rbp を保存してから rsp の値をセットします。
![Save rbp by enter() Save rbp by enter()](/images/openjdk-frame/generate-stub-code1.jpg)
ここで thread, parameter size というのが出現しているのですが、これは call_stub 呼び出し時に渡した 7, 8 番目の引数です。javaCalls 内の該当コードを再掲しておきます。CHECK がちゃんと理解できていないのですが、ここで thread にあたるものを渡しているようです。なおここでの呼出規約は x86 calling conventions でいう System V AMD64 ABI 環境という想定です。
// in src/hotspot/share/runtime/javaCalls.cpp
void JavaCalls::call_helper(JavaValue* result, const methodHandle& method, JavaCallArguments* args, TRAPS) {
...
StubRoutines::call_stub()(
(address)&link,
// (intptr_t*)&(result->_value), // see NOTE above (compiler problem)
result_val_address, // see NOTE above (compiler problem)
result_type,
method(),
entry_point,
args->parameters(),
args->size_of_parameters(),
CHECK
);
...
}
2. レジスタの保存
ひたすらレジスタをスタックに保存しています。 図中 c_rarg0, 1, 2, 3, 4, 5 はそれぞれいまの呼出規約では rdi, rsi, rdx, rcx, r8, r9 です。
![Save registers Save registers](/images/openjdk-frame/generate-stub-code2.jpg)
なお mxcsr? レジスタの保存もしているのですがそこはあまりわかっていません。
3. thread, heapbase 用レジスタの設定
interpret 中は用途が決まっているレジスタがいくつかあるようで、そのうち r12, r15 レジスタがそれぞれ heapbase, thread を指すために使用されます。
![Set thread and heapbase registers Set thread and heapbase registers](/images/openjdk-frame/generate-stub-code3.jpg)
heapbase というのは compressed oop の計算で使用されるベースアドレスのことです。compressed oop については 以前のポスト でまとめました。ここでは oop のベースアドレスをセットしているのですが、自分のメモ書きによると interpret 中に Klass 用のベースアドレスにセットし直すこともあるらしいです。
thread が具体的に何を指しているのかは調査不足です。
4. parameters の用意
interpret したいメソッドパラメータの設定をここで行います。 パラメータやその数は call_stub の caller で用意しているのでそれを使用します。
![Prepare parameters Prepare parameters](/images/openjdk-frame/generate-stub-code4.jpg)
5. entrypoint 呼出
最後に call_stub から normal_entry への jmp です。 call_stub では caller から entry_point としてアドレスが渡されているのでそれを使用します。
またここでは r13, rbx レジスタにそれぞれ sender sp, Method *
を設定しています。
これにより上で見た normal_entry が期待する通りのスタック、レジスタを用意しています。
![Call entrypoint Call entrypoint](/images/openjdk-frame/generate-stub-code5.jpg)
最後に Locals クラスを使用して entrypoint 移行直前のスタック、レジスタの様子を gdb で見てみます。 call 前なので図と違って return address をまだスタックに積んでいない状態です。
# breakpoint before jmp to entrypoint
(gdb) p $rbp
$1 = (void *) 0x7ffff59ed960
(gdb) p $rsp
$2 = (void *) 0x7ffff59ed8f8
(gdb) p ($rbp - $rsp) / 8
$3 = 13
(gdb) x /120xb $rsp
0x7ffff59ed8f8: 0xb8 0x56 0x6f 0x19 0x07 0x00 0x00 0x00 # param1 <- rsp
0x7ffff59ed900: 0xa0 0x1f 0x00 0x00 0xff 0x7f 0x00 0x00 # mxcsr?
0x7ffff59ed908: 0x40 0xda 0x9e 0xf5 0xff 0x7f 0x00 0x00 # saved r15
0x7ffff59ed910: 0x00 0xdc 0x9e 0xf5 0xff 0x7f 0x00 0x00 # saved r14
0x7ffff59ed918: 0x40 0xdb 0x9e 0xf5 0xff 0x7f 0x00 0x00 # saved r13
0x7ffff59ed920: 0x00 0xa8 0x01 0xf0 0xff 0x7f 0x00 0x00 # saved r12
0x7ffff59ed928: 0xe0 0xdc 0x9e 0xf5 0xff 0x7f 0x00 0x00 # saved rbx
0x7ffff59ed930: 0x40 0xda 0x9e 0xf5 0xff 0x7f 0x00 0x00 # call_wrapper (from c_rarg0)
0x7ffff59ed938: 0xe8 0xdc 0x9e 0xf5 0xff 0x7f 0x00 0x00 # result (from c_rarg1)
0x7ffff59ed940: 0x0a 0x00 0x00 0x00 0xff 0x7f 0x00 0x00 # result_type (from c_rarg2)
0x7ffff59ed948: 0x88 0x53 0x3a 0xd1 0xff 0x7f 0x00 0x00 # method (from c_rarg3)
0x7ffff59ed950: 0x40 0x61 0x01 0xe1 0xff 0x7f 0x00 0x00 # entrypoint (from c_rarg4)
0x7ffff59ed958: 0x10 0xdc 0x9e 0xf5 0xff 0x7f 0x00 0x00 # parameters (from c_rarg5)
0x7ffff59ed960: 0xc0 0xda 0x9e 0xf5 0xff 0x7f 0x00 0x00 # <- rbp
0x7ffff59ed968: 0xaa 0xa6 0xa8 0xf6 0xff 0x7f 0x00 0x00
# check param
(gdb) x /40xb 0x07196f56b8
0x7196f56b8: 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7196f56c0: 0xa0 0x5c 0x02 0x00 0x02 0x00 0x00 0x00
0x7196f56c8: 0xdd 0xea 0x2d 0xe3 0xe6 0xea 0x2d 0xe3
0x7196f56d0: 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7196f56d8: 0x40 0x08 0x00 0x00 0x03 0x00 0x00 0x00
# check registers
(gdb) p /x $r12
$4 = 0x0
(gdb) p /x $r15
$5 = 0x7ffff001a800
(gdb) p /x $r13
$6 = 0x7ffff59ed8f8
(gdb) p /x $rbp
$10 = (void *) 0x7ffff59ed960
(gdb) p ((Method *) $rbx)->_constMethod->_constants->_pool_holder->_name->as_C_string()
$9 = 0x7ffff00193d0 "Locals"
本当は return 部分の処理もまとめたいところなのですが力尽きました。