【解説】Intel CPUの拡張命令群FREDについて(その1)

今回はIntel CPUの新しい拡張命令 FRED (Flexible Return and Event Delivery) について解説します。
FREDは、既存のリング遷移命令(特権レベルを変更する命令)をシンプルな形で置き換えるために作られました。

Linux 6.9にマージ済で、Clearwater Forest/Panther Lake以降に利用可能となる予定です。

結論(3行まとめ)

  • リング遷移命令が複雑化しているのに事実上0と3しか使われていない
  • 全てのリング遷移命令を低レイテンシなFRED命令に置き換えることで効率化
  • Linux 6.9 + Clearwater Forest/Panther Lake以降で利用可能

内容

現状の課題

リング遷移命令が多く、複雑化している

x86アーキテクチャではリングと呼ばれる仕組みを使って特権レベルを制御しています。
リングには0~3まで4段階あり、数値が低いほど特権レベルが高いです。



CPUはI/Oの発行や他セグメントへアクセスをする際に、現在実行している権限レベル(CPL / Current Privilege Level)と対象の権限レベル1を比較し、対象より低い数値(高い権限)を持っているなら実行を許可します。

基本的にカーネルはリング0、ユーザーはリング3で動作しており、カーネルとユーザー空間で行き来する場合はリング遷移が行われます。
このリング遷移手法について、現在は様々な歴史的経緯が重なって複雑化しています。
以下にリング遷移手法について解説します。

IDT経由の割り込み/トラップゲート

INT命令による能動的な割り込みや、例外発生などによる受動的な割り込みを受けた際、CPUはIDT(Interrupt Descriptor Table)と呼ばれるテーブルを参照して割り込みベクタに応じたハンドラを呼び出します。


よくある例としては、ユーザー空間でプロセスを実行している際にタイマ割り込みが発生してカーネル空間に処理が移り、プロセスの切替が行われたりします。
また、古いCPUやカーネルではEAXにシステムコール番号を入れてINT 0x80を発行することでシステムコールを実装している場合があり、これも割り込みによるリング遷移に含まれます。

そして割り込み処理を終えた後、IRET 命令を実行して元に戻す際にもリング遷移が発生します。

リング遷移を含む far call, far jmp, far ret

こちらは最近のカーネルではあまり見かけないかもしれません。
最近はメモリ管理をセグメントではなくページングで行うようになったため、セグメントをまたぐジャンプやコール命令は基本的に実行されません。

とはいえ仕様にある以上、使われないとしてもIntelとしては実装しないといけないのが辛いですね。
x86にはこういった過去の遺産が山ほど眠っています。

余談ですがIntelは過去の遺産を削った X86-S というのも提案していたのですが、こちらは頓挫したようです。Tom’s Hardware, “Intel terminates x86S initiative”
言ってしまえば今回のFREDも過去の遺産をどうにかしたい!というのがきっかけっぽいですけど…
RISC-VやARMが台頭してきている中、設計を効率化するために重い腰を上げた感じでしょうか。

sysenter, sysexit, syscall, sysret によるシステムコール

近年のCPUは基本的にこれらの命令を利用してシステムコールを発行します。

Intel CPUでは32bitの場合sysenter/sysexit, 64bitの場合はこれに加えてsyscall/sysret が利用できます。
AMD CPUでは32bitの場合syscall/sysretとsysenter/sysexit、64bitの場合はsyscall/sysretのみを利用できます。ややこしい

システムコールはユーザーとカーネルを行き来する最も一般的な手法で、これによるリング遷移は頻繁に発生します。

事実上リング0とリング3しか使われていない

先述した通り、リング遷移は歴史的経緯が重なり複雑化しています。
一方で現状まともに使われてるのはカーネルのリング0とユーザーのリング3ぐらいです。
これについては X86-S でも深く言及しており、Intel的にかなり邪魔だと推測できます。

今はハイパーバイザーやデバイスドライバーでリング1,2を有効的に使おうという話も出ているようですが、どれも本質的ではないという状態です。
というのも、MIPSではリングが2種類しかなかったりRISC-Vでは3種類しかなかったりするので、結局様々なアーキテクチャでOSを動かそうとしたら本質的はユーザーとカーネルの二分割しかできないという問題があるためです。

FREDアーキテクチャの概要

FRED拡張ではイベントデリバリーというシステムと、2種類のFREDリターン命令から構成されます。

FRED拡張を有効にするとFRED命令が唯一のリング遷移手法となります。
また、イベントデリバリーはリング0のみ、リターン命令はリング3のみを対象とするため、リング1, 2には入れなくなります。

FREDイベントデリバリー

割り込みや例外は既存のデータ構造(IDTなど)にアクセスせず、全てFREDイベントデリバリーに置き換えられます。
またSYSENTER/SYSCALL命令によるシステムコール発行も、既存の動作に代わってFREDイベントデリバリーを使用するように変更されます。

FREDリターン命令

2つの新しいリターン命令が実装されます。

ERETS

リング0内でイベントデリバリーからリターンします。
主な用途としては、カーネル空間で割り込みが発生した際のリターンに利用します。

ERETU

リング0からリング3へリターンを行います。
システムコールや、ユーザープロセス実行中に発生した割り込みのリターンなどに利用します。

スタックレベル

FREDイベントデリバリー発生時のスタックについては、4つまで使い分けることが可能です。
arch/x86/kernel/fred.c#L9-L20

/* #DB in the kernel would imply the use of a kernel debugger. */
#define FRED_DB_STACK_LEVEL		1UL
#define FRED_NMI_STACK_LEVEL		2UL
#define FRED_MC_STACK_LEVEL		2UL
/*
 * #DF is the highest level because a #DF means "something went wrong
 * *while delivering an exception*." The number of cases for which that
 * can happen with FRED is drastically reduced and basically amounts to
 * "the stack you pointed me to is broken." Thus, always change stacks
 * on #DF, which means it should be at the highest level.
 */
#define FRED_DF_STACK_LEVEL		3UL

/* Must be called after setup_cpu_entry_areas() */
void cpu_init_fred_rsps(void)
{
	/*
	 * The purpose of separate stacks for NMI, #DB and #MC *in the kernel*
	 * (remember that user space faults are always taken on stack level 0)
	 * is to avoid overflowing the kernel stack.
	 */
	wrmsrl(MSR_IA32_FRED_STKLVLS,
	       FRED_STKLVL(X86_TRAP_DB,  FRED_DB_STACK_LEVEL) |
	       FRED_STKLVL(X86_TRAP_NMI, FRED_NMI_STACK_LEVEL) |
	       FRED_STKLVL(X86_TRAP_MC,  FRED_MC_STACK_LEVEL) |
	       FRED_STKLVL(X86_TRAP_DF,  FRED_DF_STACK_LEVEL));

	/* The FRED equivalents to IST stacks... */
	wrmsrl(MSR_IA32_FRED_RSP1, __this_cpu_ist_top_va(DB));
	wrmsrl(MSR_IA32_FRED_RSP2, __this_cpu_ist_top_va(NMI));
	wrmsrl(MSR_IA32_FRED_RSP3, __this_cpu_ist_top_va(DF));
}

例えばイベントを処理している途中に追加で割り込みが発生した場合、より重要度の高い割り込みならそちらを優先する必要があります。
この際にカーネルスタックをわけることでスタックの競合や破損を防ぐことができます。
Linuxではを #DB (Debug), #NMI (Non-Maskable Interrupts), #MC (Machine Check Exception), #DF (Double Fault) で使い分けているようです。

利用するスタックレベルが決まっているイベントと、自由に定めることができるイベントがあるようです。

5.1.2 Determining the New Values for Stack Level, RSP, and SSP(翻訳)

FREDは、リング0で使用するための4つの異なるスタックをサポートしています。CPUは、現在使用中のスタックを2ビットの値である現在のスタックレベル(CSL)で識別します。FREDイベント配信は、まずイベントのスタックレベルを決定し、それに基づいてCSLを変更するかどうかを判断します。イベントのスタックレベルは、CPL、イベントの性質とタイプ、イベントのベクトル(一部のイベントタイプの場合)、およびシステムソフトウェアによって設定されたMSRに基づいて決定されます:

  • イベントがリング3で発生し、イベント配信中に発生したネストされた例外やダブルフォールト(#DF)でない場合、イベントのスタックレベルは0です。
  • イベントがリング0で発生した場合、イベント配信中に発生したネストされた例外、または#DFである場合、以下の項目が適用されます:
    • イベントがマスク可能な割り込みの場合、イベントのスタックレベルはIA32_FRED_CONFIG[10:9]です。
    • イベントが例外または非マスク可能割り込み(NMI)の場合、イベントのスタックレベルはIA32_FRED_STKLVLS[2v+1:2v]です。ここで、vはイベントのベクトル(0〜31の範囲)です。
    • その他のすべてのイベントのスタックレベルは0です。

イベントがリング3で発生した場合、新しいスタックレベルはイベントのスタックレベルです。それ以外の場合、新しいスタックレベルはCSLとイベントのスタックレレベルの最大値です。(これは、リング3で発生したイベントのFREDイベント配信後、CSLが常に0であることを意味します。ただし、イベントがネストされた例外または#DFでない場合を除きます。)

新しいスタックレベルを決定した後、FREDイベント配信は新しいRSP値を以下のように識別します:

  • CPLまたはCSLのいずれかが変更される場合、新しいRSP値は新しいスタックレベルに対応するFRED RSP MSRの値になります。
  • それ以外の場合、新しいRSP値は現在のRSP値からIA32_FRED_CONFIG & 1C0H(そのMSRのビット8:6の64の倍数)を減算し、64バイト境界にアライメントされます(RSP[5:0]をクリアすることによって)。

スーパーバイザーシャドウスタックが有効な場合、新しいSSP値は以下のように決定されます:

それ以外の場合、新しいSSP値は現在のSSP値からIA32_FRED_CONFIG & 8(そのMSRのビット3を設定するとSSPが8減算されることを示します)を減算した値になります。

CPLまたはCSLのいずれかが変更される場合、新しいSSP値は新しいスタックレベルに対応するFRED SSP MSRの値になります。新しいSSP値が8バイトアライメントされていない場合、FREDイベント配信は一般保護フォールト(#GP)を引き起こします。

FREDアーキテクチャの詳細仕様

詳細な仕様について、長くなってしまったので次の記事に回しました。
FRED の CPU内部処理について仕様書に書いてある細かい挙動まで解説しています。

【解説】Intel CPUの拡張命令群FREDについて(その2)

参考サイト

  1. 例えばI/OならIOPL (IO Privilege Level, EFLAGS[13:12]) を、 他セグメントへのアクセスなら DPL / Descriptor Privilege Level) を確認します ↩︎

シェアする

  • このエントリーをはてなブックマークに追加

フォローする