前回:【解説】Intel CPUの拡張命令群FREDについて(その1)
詳細解説が長くなったので分割しました。
目次
FREDアーキテクチャの詳細仕様
有効化
CPUID.(EAX=7, ECX=1) : EAX.FRED[bit 17] でFRED拡張に対応しているか確認できます。
その後、CR4[32] (CR4.FRED) ビットを建てることでFRED拡張が有効化されます。
設定(新規MSR)
設定には主にMSRを利用します。
MSRはCR4.FRED無効時にもセット可能なので、セットしてから有効化しましょう。
IA32_FRED_CONFIG(0x1D4)
- [1:0] CSL (Current Stack Level)
- [2] 予約済
- [3] スタック変更が無い場合、シャドウスタック(SSP)を8減算します
- [5:4] 予約済
- [8:6] スタック変更が無い場合、通常スタック(RSP)を指定量減算します(64バイト単位)
- [10:9] リング0で発生したマスク可能な割り込みを実行するスタックレベル
- [11] 予約済
- [63:12] FREDイベントハンドラのアドレス。イベント発生時はRIPがこの値にセットされます。
IA32_FRED_RSP[0-3](0x1CC-0x1CF)
対応するスタックレベルの通常スタックポインタ(RSP)を記録します。
IA32_FRED_STKLVLS(0x1D0)
2bit × 32個の配列として解釈されます。
リング0で発生するベクトル N の例外に対して、利用するスタックレベルを IA32_FRED_STKLVLS[2N+1:2N]とします(0≦N≦31)
IA32_FRED_SSP[1-3](0x1D1-0x1D3)
対応するスタックレベルのシャドウスタックポインタ(SSP)を記録します。
レベル0のSSPは既存のMSR(IA32_PL0_SSP, 0x6A4)を流用します。
IA32_FRED_SSP0と表記されたらIA32_PL0_SSPのことです。
設定(既存MSR)
IA32_STAR
もともと SYSCALL/SYSRET で利用されるMSRです。
SYSCALL 命令では IA32_STAR[47:32] から派生した値を使用してカーネルの CS/SS をロードし、SYSRET 命令では IA32_STAR[63:48] から派生した値を使用してユーザーの CS/SS をロードしていました。
https://wiki.osdev.org/SYSENTER
FRED でも同様に、イベントデリバリーが発生する際に IA32_STAR[47:32] からカーネルの CS/SS をロードし、ERETU 命令で IA32_STAR[63:48] からユーザーの CS/SS をロードします。
IA32_KERNEL_GS_BASE
もともと SWAPGS 命令で利用される MSR です。
SWAPGS 命令を発行すると、この MSR の値と GS のベースアドレスを交換します。
FRED では ERETU 命令の実行時、およびリング3からイベントを発行する際に同じ交換操作が行われます。
IA32_PL0_SSP
先述した通り、スタックレベル0のSSPとして再利用されます。
イベントデリバリー発生時の挙動
プログラムカウンタ(RIP)の切替
イベントが発生したときの特権レベル(CPL)により、2種類のエントリーポイントが用意されます。
リング3で発生した場合は、RIPは「IA32_FRED_CONFIG & ~0xFFF」で設定されます。
リング0の場合はこの値に256を加えたものがRIPに設定されます。
スタックポインタ(RSP)の切替
イベント発生時のスタックレベルは発生元のCPL(Current Stack Level)、イベントの種類、ベクタ、さらにMSRで設定された値により決定されます。
・リング3で発生した場合はスタックレベル0です(#DF除く)
・リング0で発生した場合、マスク可能割り込みなら IA32_FRED_CONFIG[10:9] の値、
例外やNMIの場合はベクトル N に対して IA32_FRED_STKLVLS[2N+1:2N] の値、
それ以外はスタックレベル0を利用します。
RSP と SSP については、スタックレベルが変更されない場合、MSR の設定に基づいて減算されます。
セグメント(CS/SS/GS)の切替
リング3で発生したイベントの場合、IA32_STAR[47:32] からカーネル用の CS をロードします。
また IA32_STAR[47:32] + 8 からカーネル用の SS をロードします。
GSベースは IA32_KERNEL_GS_BASE の値と交換されます。
リング0で発生した場合、CS/SS/GS は変更されません。
旧コンテキスト情報の保存
FREDイベントデリバリーは、イベントハンドラのスタック上に旧コンテキスト情報を保存します。
内容はIDTを利用した割り込みと基本的には同じフォーマットで、一部の補助情報が追加されています。
FREDイベントデリバリーを発行すると、スタックに以下のような64バイトのフレームを生成します。
FRED Frame (64Byte)
[63:56] 予約済み
[55:48] イベントデータ
[47:40] 拡張SS (Stack Segment)
[39:32] 元のスタックポインタ (RSP)
[31:24] 元のRFLAGS
[23:16] 拡張CS (Code Segment)
[15:08] 元のプログラムカウンタ (RIP)
[07:00] エラーコード
イベントデータ
この領域は、発生したイベントの種類に応じた追加データを保持します。
- ページフォールト(#PF)の場合
- フォールトした線形アドレス(CR2と同一)が保存されます。
- デバッグ例外(#DB)の場合
- 以下のビットフィールドで構成され、各ビットはデバッグ例外の原因を示します。
- [3:0] 各ブレークポイント条件が満たされたかどうか(DR7参照)
- [10:4] 未定義
- [11] バスロックの獲得による例外であれば1に設定(BLD)
- [12] 未定義
- [13] デバッグレジスタアクセスが検出された場合に1(BD)
- [14] シングルステップ実行(RFLAGS.TF=1)による例外の場合に1(BS)
- [15] 未定義
- [16] RTM領域内で発生した例外の場合に1(RTM)
- [63:17] 未定義
- 以下のビットフィールドで構成され、各ビットはデバッグ例外の原因を示します。
- デバイス利用不可例外(#NM)の場合
- 拡張機能の無効化が原因の場合、IA32_XFD_ERR から読み出された値が保存されます。
- それ以外の場合はゼロが格納されます。
- その他のイベントの場合
- 現時点では定義されておらず、ゼロが格納されます。
拡張SS, 拡張CS
- 拡張SS
- [15:0] 元のSSセレクタ
- [16] ハードウェア例外の場合、STIによる割り込みブロックが有効だった場合に1
- [17] SYSCALL, SYSENTER, INT 命令の場合に1
- [18] #NMIの場合に1
- [31:19] 未定義
- [39:32] イベントのベクタ N を記録します。
- SYSCALL=1, SYSENTER=2
- [47:40] 未定義
- [51:48] イベントタイプを記録します
- 0:外部割り込み
- 2:NMI
- 3:ハードウェア例外(#PFなど)
- 4:ソフトウェア割り込み(INT N)
- 5:特権ソフトウェア例外(INT 1)
- 6:ソフトウェア例外(INT3 or INTO)
- 7:その他(SYSCALLやSYSENTERの場合)
- [55:52] 未定義
- [56] イベントがエンクレーブ実行(Intel SGXなど)に関連している場合に1
- [57] イベント発生時に論理プロセッサが64ビットモードであった場合に1
- [58] 別のFREDイベント処理中に発生したネスト例外である場合に1(#DFを除く)
- [59] 未定義
- [63:60] SYSCALL, SYSENTER, INT命令の場合、命令長を保存する。それ以外は0
- 拡張CS
- [15:0] 元のCSセレクタ
- [17:16] リング0で発生したイベントの場合、発生時のスタックレベル(CSL)
- リング3で発生した場合は0
- [18] リング0で発生し、WAIT_FOR_ENDBRANCH 状態であった場合に1
- 具体的には以下の条件を全て満たした時
- CPL = 0
- CR4.CET = 1
- IA32_S_CET.ENDBR_EN = 1
- IA32_S_CET.TRACKER = 1
- ただし SYSCALL、SYSENTER、INT命令のイベントは常にこのビットをクリアします。
- 具体的には以下の条件を全て満たした時
- [63:19] 未定義
エラーコード
下位16bitにエラーコードが格納されます。上位ビットは未定義(ゼロ)です。
その他の状態変更
ユーザーシャドウスタックが有効な場合
イベントがリング3で発生し、ユーザーシャドウスタックが有効な場合、IA32_PL3_SSP に古いSSPの値がロードされます。
スーパーバイザーの間接分岐保護機能(IBT)が有効な場合
IA32_S_CET が更新され、TRACKERの値が WAIT_FOR_ENDBRANCH に設定され、SUPPRESS ビットが0にクリアされます。ソフトウェアは、新しいRIP値が参照する命令が ENDBR64 であることを確認する必要があります。
#NMIのブロック
FREDイベント配信が#NMIを配信する場合、NMIがブロックされます。
デバッグトラップの処理
他のFREDイベントが発行される時点でデバッグトラップが保留中である場合があります。
この場合、元のイベント発生時に保留中であったデバッグトラップをすべて破棄します。
FREDイベント配信中のデータブレークポイント
FREDイベント配信中に検出されたデータブレークポイントは破棄されず、FREDイベント配信完了後も保留中である可能性があります。
リターン時の挙動(ERETS)
ERETS (Event Return to Supervisor) はリング0のままイベントハンドラから復帰します。
リング0内で完結するため、CS, SS, GSは変更しません。
スタックフレームのチェック
FREDイベントを発行した際に生成したスタックフレームを参考に元に戻します。
この際、フレームの各要素とシャドウスタックに対して正常性チェックが行われます。
基本的にフレームに触れなければエラーを起こすことは無いのでここでは割愛します。
興味のある方は 6.1.1 Loading and Checking the Return State,
6.1.2 Checking the Shadow Stack の部分を参照してください。
リターンの実行
RIP, RFLAGS, RSP, CSL をスタックからポップされた値でロードします。
シャドウスタックが有効な場合、SSP はシャドウスタックからポップされた値でロードされます。
拡張CSおよび拡張SSの値に基づいて、以下のステップが追加で実行されます。
- 拡張CS[18] = 1 の時、ERETSの完了時に WAIT_FOR_ENDBRANCH 状態になります。
- 拡張SS[16] = 1 の時、RFLAGS.IF = 1で完了し、STIによるブロッキングが有効になります。
- 拡張SS[17] = 1 の時、RFLAGS.TF = 1で完了し、シングルステップトラップが保留状態になります。
- 拡張SS[18] = 1 の時、ERETSはNMIをアンブロックします。
リターン時の挙動(ERETU)
ERETU (Event Return to User) はリング0からリング3に復帰します。
スタックフレームのチェックについてはERETSと同じ理由で割愛します。
興味のある方は 6.2.1 Loading and Checking the Return State,
6.2.2 Checking the Shadow Stack の部分を参照してください。
リターンの実行
スタックフレームから RIP, RFLAGS, RSP, CS, SS をロードします。
ユーザシャドウスタックが有効な場合、SSPは IA32_PL3_SSP からロードされます。
GS は IA32_KERNEL_GS_BASE の値と交換します。
拡張SSの値に基づいて、以下のステップが追加で実行されます。
- 拡張SS[17] = 1 の時、RFLAGS.TF = 1で完了し、シングルステップトラップが保留状態になります。
- 拡張SS[18] = 1 の時、ERETSはNMIをアンブロックします。
※ DS, ES, FS, GSについて
IRET を実行するとき、これらのセグメントの DPL < 3 だった場合は Null にしていました。これはリング3の状態から高い権限のセグメントにアクセスできないようにするためです。
一方で ERETU は IRET と異なり Null にしません。セグメントの権限確認はカーネルに委ねられます。
命令順序保証
SYSRET と同じく命令順序保証を提供します。
具体的には、ERETUの実行後に続く命令は、以前の命令の実行が完了する前にメモリからフェッチされる可能性はありますが、ERETU以前のすべての命令が実行を完了するまで(投機的実行を含み)実行されません。
既存命令への影響
#UDになる命令
FREDを有効化すると、以下の命令は無効なオペコード例外(#UD)になります。
- SWAPGS
- FRED時に自動で実行するように変更されるため
- CLRSSBSY, SETSSBSY
- スーパーバイザシャドウスタックトークンを操作する必要が無いため
- SYSEXIT, SYSRET
- ERETU に代替されるため
#GPになる命令
FREDを有効化すると、以下の命令は一般保護例外(#GP)になります。
- コールゲートを参照する(リング遷移の発生する) far CALL, far JMP
- 対象CSのRPL値がCPLよりも大きい(リング遷移の発生する) IRET, far RET
INT命令
INT命令を実行するとFREDイベントが発行されます。
- INT N(0xCD, N)
- 256個のソフトウェア割り込み命令
- FREDフレームの拡張SS[17] == 1 で判断可能
- N は 拡張SS[39:32] に格納
- イベントタイプは 拡張SS[51:48] == 4(ソフト割込)
- INT3(op 0xCC)
- #BP(ブレークポイント例外)を生成します
- イベントタイプは 拡張SS[51:48] == 6(ソフト例外)
- INTO(0xCE)
- RFLAGS.OF = 1の場合、#OF(オーバーフロー例外)を生成します(IA-32eのみ)
- イベントタイプは 拡張SS[51:48] == 6(ソフト例外)
- INT1(0xF1)
- #DB(デバッグ例外)を生成します
- イベントタイプは 拡張SS[51:48] == 5(特権ソフト例外)
SYSCALL / SYSENTER
本来なら IA32_STAR などのMSRを参照するのですが、FREDイベント発行に置き換えられます。
拡張SS[39:32] == 1 or 2, OTHERタイプ(拡張SS[51:48] == 7)のFREDイベントを発行します。
リターンする場合は、SYSRET / SYSEXIT の代わりにERETUを利用します。
ユーザー空間への影響
気にする必要はありません。
Kconfigで設定すれば、対応してるアーキテクチャならOS側で処理してくれます。
kernel-x86_64-rhel.config#L8032
実装について
ソフトウェア側は Linux 6.9 でマージされました。
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=720c857907530e6cdc86c9bc1102ea6b372fbfb6
ハードウェア的に対応するのは Panther Lake, Clearwater Forest 以降のようです。