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 に戻る回数を減らすためのはず
  • 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_entryi2i_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
開始時のスタックとレジスタ
  • 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* から計算します。

Prepate locals
locals の用意
  • 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()

残りの frame を設定します。あまり理解のできていない要素もいくつかありますが図にまとめました。

Prepare frame values
残りの frame の値を設定
  • 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 実装では存在する

この時点の 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()
enter() で rbp の保存

ここで 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
レジスタの保存

なお mxcsr? レジスタの保存もしているのですがそこはあまりわかっていません。

3. thread, heapbase 用レジスタの設定

interpret 中は用途が決まっているレジスタがいくつかあるようで、そのうち r12, r15 レジスタがそれぞれ heapbase, thread を指すために使用されます。

Set thread and heapbase registers
thread, heapbase 用レジスタの設定

heapbase というのは compressed oop の計算で使用されるベースアドレスのことです。compressed oop については 以前のポスト でまとめました。ここでは oop のベースアドレスをセットしているのですが、自分のメモ書きによると interpret 中に Klass 用のベースアドレスにセットし直すこともあるらしいです。

thread が具体的に何を指しているのかは調査不足です。

4. parameters の用意

interpret したいメソッドパラメータの設定をここで行います。 パラメータやその数は call_stub の caller で用意しているのでそれを使用します。

Prepare parameters
parameters の用意

5. entrypoint 呼出

最後に call_stub から normal_entry への jmp です。 call_stub では caller から entry_point としてアドレスが渡されているのでそれを使用します。

またここでは r13, rbx レジスタにそれぞれ sender sp, Method * を設定しています。

これにより上で見た normal_entry が期待する通りのスタック、レジスタを用意しています。

Call entrypoint
entrypoint の呼出

最後に 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 部分の処理もまとめたいところなのですが力尽きました。