メインコンテンツまでスキップ

StackMapFrame と検証

JVM はクラスの検証で,命令が要求する型やスタックの高さなどを調べます。StackMapTable 属性のフレームは,メソッド内の特定位置におけるローカル変数とスタックの型情報です。実行中の値を記録するデバッガのフレーム表示とは別のものです。

JAL の通常のコンパイルではスタックマップを生成します。ただし,誤った命令列を正しい処理へ書き換える機能ではありません。コンパイルの成功と,対象 JVM での検証・実行の成功も区別してください。

合流する経路の状態をそろえる

public static choose(I)I {
iload_0
ifeq Zero
iconst_1
goto Join
Zero:
iconst_0
Join:
ireturn
}

どちらの経路でも,Join へ来たときに int が一つ積まれています。そのため,同じ ireturn で値を返せます。

片方の経路で値を積まなければ,高さが合いません。片方に int,もう片方に参照を積んでも,そのまま同じ整数演算には使えません。参照型同士は同一のクラスである必要はありませんが,合流後に使う型へ代入可能であることが必要です。

合流後に読み出すローカル変数には,そこへ至るすべての経路で,読み出す命令に適した型の値が設定されている必要があります。一方の経路だけで代入したスロットを,合流後に無条件で読み出すことはできません。

例外ハンドラの入口

例外ハンドラの入口には,例外オブジェクト一つだけが積まれます。通常経路の途中のスタックがそのまま渡されるわけではありません。

ハンドラから通常経路へ合流させるなら,astore または pop で例外を取り出し,合流先の高さと型を合わせます。ハンドラで例外を取り出して戻る例は,例外処理を参照してください。

未初期化参照

new の直後は,通常のインスタンスとして使えない未初期化参照です。適切な <init> を呼ぶ前に,通常のインスタンスメソッドを呼んだり,初期化済みの戻り値として返したりすることはできません。

コンストラクタ内の this にも初期化前の制約があります。親または同じクラスのコンストラクタを正しく呼び,初期化を完了させます。

戻り命令とスタックの残り

ireturn は戻り値に使う int を必要とし,areturn は戻り型に代入可能な参照を必要とします。値を返さないメソッドには return を使います。

戻り命令の際にはフレームが破棄されます。したがって,余分な値がスタックに残っているだけで常に JVM の検証エラーになる,という説明は正しくありません。不要な値を poppop2 で片付けるのは,途中の状態を明確にする書き方です。分岐の合流や後続の命令で要求される整合性とは分けて考えます。

エラーを調べる

報告された命令の直前で,スタックの型と個数,読み出すローカル変数の番号を確認します。分岐先なら,そこへ入る各経路も確認してください。

Jaspera の「問題」や「グラフ」は調査に使えます。解析結果だけで判断できない場合は,出力したクラスを対象の JVM で実行し,javap -v で命令と StackMapTable を調べられます。

検証規則は JVM 仕様 §4.10,戻り命令の動作は §6.5 return に記載されています。