int 2E/sysenter/syscall考察

WindowsNT/2000/XP/2003などにおいてカーネルモードへ移行するために使われているint 2E/sysenter/syscallの3命令について、考えてみます。

(本記事の初稿は2005年です。64ビットCPUが普及した現在でも有用な内容と思われるので、そのままの内容で公開します。)

■カーネルモードへ移行する方法
各種Windowsにおいてカーネルモードへ移行するために使われている方法は以下のようになります。

int 2E(割込みゲート)とcall(コールゲート)は32ビット世代の最初のCPUである386から利用可能な方法です。(正確にはプロテクトモードが導入された286(16ビットCPU)からですが。)これに対し、sysenterはインテルがPentiumIIで導入した命令、syscallはAMDが(たしか)K6で導入した命令です。なおsyscallはEM64Tでも利用可能です。

■速度比較
これらの方法で速度にどの程度の差があるのかをテストしてみます。以下のプログラムを使います。

VirtualQuery()とFlushInstructionCache()の2つのAPIを100万回実行します。どちらもカーネルモードへ移行して処理の行われるAPIです。この2つのAPIを選んだのは、カーネルモード内で行われる処理が簡単そうで、OSのバージョンが違っても処理内容(=速度)に違いが出にくいように思われたからです。上記プログラムを実行した結果を示します。(20回実行した結果の平均です。)

(1)と(2)の比較より、int 2Eよりはsysenterの方が速そうだとわかります。(3)はWOW64の処理(Win32でのAPI呼び出しをWin64でのAPIに置き換える処理)が入るのでパスするとして、(2)と(4)を比べると、かなり差が出てしまっています。ただこれを見て「syscallはsysenterよりも優れている」と判断するのは早計です。syscallとsysenterには、処理内容で優劣の差はありません。(詳細は後述。)AMD64ではsysenterよりもsyscallの方がCPU内部での最適化が進んでいるといった程度の違いなのでは、と推測されます。(2)と(5)の違いには、正直びっくりしました。Netburstアーキテクチャの弱点が露呈したものなのか、それともメモリアクセス性能の違い(Pentium4の方はFSB533MHz, Athlon64の方はHyperTransport 800MHz(FSB換算で1600MHz相当))なのか、私にはわかりません。

以下ではint 2Eとsysenter/syscallの概要について調べた後、KD(カーネルデバッガ)を用いてWindowsがこれらの命令をどのように使っているのかを調べてみます。

■int 2Eの概要
ユーザーモードにおいてint 2Eを実行すると、CPUは以下のような処理を行います。

カーネルモードのルーチンを指すセレクタを作りそのディスクリプタをGDT(グローバル・ディスクリプタテーブル)に入れておきます。そのセレクタを指すゲートディスクリプタ(割込みゲート)を作成することで、ソフトウェア割込みを使ってカーネルモードに移行できるわけです。

■sysenter/syscallの概要
int 2Eを実行すると割り込みゲート経由でカーネルモードに移行するわけですが、この移行に際して以下のような処理が暗黙のうちに行われます。

これらの処理をするために相当のオーバーヘッドがかかるであろうことが想像できます。特にシステムコールする毎にセレクタのチェックをするのは無駄と言えます。なぜなら、OSのカーネルモード・ルーチンのエントリーポイントはどのAPI呼び出しであろうとも変わらないからです。(呼び出したいサービスの番号をレジスタに入れ、カーネルモードのディスパッチャを呼び出すわけですから。)

これらの問題を解決するために導入されたのがsysenter/syscall命令です。この2つの命令はフラットメモリ・モデルの利用を前提に高速なシステムコールを実現します。sysenterはインテルがPentiumIIで導入した命令、
syscallはAMDが(たしか)K6で導入した命令です。なおsysenterとsyscallとで、処理内容に優劣の差はありません。
以下に述べるように、参照しているレジスタ(MSR)が違う程度です。似た内容の命令が2種類あるのは、(当方の勝手な推測では)インテルとAMDとの間の政治的な理由(おたがいのメンツ)に起因するものと思われます。

sysenter/syscallでは、カーネルモードのルーチンを指すセレクタをあらかじめ指定のレジスタ(MSR)に入れておき、実際にカーネルモードに移行する際(sysenter/syscall実行時)にはそのレジスタからノーチェックでCSレジスタにロードすることで、オーバーヘッドの少ないカーネルモード移行を実現します。

たったこれだけのステップでカーネルモードに移行できてしまうのです。セグメントレジスタへのセレクタのロードはノーチェックです。なおCSレジスタ・SSレジスタにロードするセレクタの属性は以下のように設定されます。(この辺りが、フラットメモリ・モデルの利用を前提にしている所以です。)

syscallについても見てみましょう。まずレガシーモード(386互換のモード)からです。

次はロングモードです。

レガシーモードとロングモードとで動作が若干異なるものの、処理内容はいずれもsysenterと概ね同じであることがわかります。

■int 2Eの実際
int 2EがWindowsでどのように使われているのかを調べてみましょう。ここからは、KD(カーネルデバッガ)を使います。Windows2000(SP4)のインストールされたPC(ターゲットPC)とホストPCとをシリアルケーブル(クロスタイプ)で接続してからターゲットPCをデバッグモードで起動します。ホストPC側からブレークをかけて調査開始です。

まずIDTのベースアドレスを調べます。

IDTのベースアドレスが0x8003f400で、リミットが0x7ffであることがわかります。これより、0x2E番目のエントリーのアドレスは8003f400+8×2e=8003f570と求まります。ダンプしてみます。

ハイライトした箇所(cd 55~ee 86 80)がIDTの0x2E番目のゲートディスクリプタです。+5バイト目が0xEE(2進数で1110_1110)となっているので、このゲートディスクリプタがタスクゲートやトラップゲートではなく割込みゲートであることがわかります。わかりやすくまとめると以下のようになります。

DPLが3なので、CPLが3(ユーザーモードのプロセス)から呼び出せるゲートディスクリプタであることがわかります。ジャンプ先のセレクタ0x8を調べます。

ベースアドレスが0x00000000、リミットが0xffffffffなコードセグメントであることがわかります。4GBの全仮想アドレス空間をアクセスできる(フラットメモリな)セレクタです。Pl(=DPL)が0なので、カーネルモード用のコードセグメントであることもわかります。さらにこのベースアドレス(0x00000000)にゲートディスクリプタのオフセット値(0x808655cd)を加算した値が0x80000000以上である、すなわち4GBの仮想アドレス空間の上位2GBを指していることより、システムが予約している領域内で実行されることもわかります。

次にTSSを調べます。まずTRレジスタの内容(セレクタ)を見ます。

GDT内の0x28番目にTSSディスクリプタが入っていることがわかります。さっそくセレクタのディスクリプタを見てみます。

たしかにTSSディスクリプタです。上記ベースアドレスを元にTSSそのものをダンプします。

ハイライトした箇所(00 3c~10 00)より、CPUがカーネルモード(RING0)に切り替わるときにSSレジスタにはセレクタ0x10、ESPレジスタには0x80873c00がロードされることがわかります。セレクタ0x10について調べます。

ベースアドレスが0x00000000、リミットが0xffffffffなデータセグメントであることがわかります。4GBの全仮想アドレス空間をアクセスできる(フラットメモリな)セレクタです。Pl(=DPL)が0なので、カーネルモード用のデータセグメントであることもわかります。

■sysenterの実際
WindowsXP Professional SP2がインストールされたターゲットPCを用いて、sysenterがWindowsでどのように使われているのかを調べてみます。

まずMSRを読み取ります。

SYSENTER_CS_MSR(MSR-174H)が0x8ですので、sysenter実行時にCSレジスタには0x8がロードされ、SSレジスタには0x8+0x8=0x10がロードされることがわかります。(セレクタの値がWindows2000の場合と同じになっています。)SYSENTER_EIP_MSR(MSR-176H)に入っている0x80865710がEIPレジスタにロードされ、SYSENTER_ESP_MSR(MSR-175H)に入っている0xf7a34000がESPレジスタにロードされることがわかります。

セレクタについて見てみます。

CSレジスタにロードされるセレクタ0x8は、ベースアドレスが0x00000000、リミットが0xffffffffなコードセグメントであることがわかります。4GBの全空間をアクセスできる(フラットメモリな)セレクタです。Pl(=DPL)が0なので、カーネルモード用のコードセグメントであることもわかります。さらにこのベースアドレス(0x00000000)にEIPレジスタにロードされる値(0x80865710)を加算した値が0x80000000以上である、すなわち4GBの仮想アドレス空間の上位2GBを指していることより、システムが予約している領域内で実行されることもわかります。

SSレジスタにロードされるセレクタ0x10は、ベースアドレスが0x00000000、リミットが0xffffffffなデータセグメントであることがわかります。4GBの全仮想アドレス空間をアクセスできる(フラットメモリな)セレクタです。Pl(=DPL)が0なので、カーネルモード用のデータセグメントであることもわかります。

■syscallの実際
WindowsXP Professional x64 Edition(RC1)がインストールされたターゲットPCを用いて、syscallがWindowsでどのように使われているのかを調べてみます。なおここで調べるのはロングモードでのsyscallについてです。

まずMSRを読み取ります。

STAR MSR(MSR-C000_0081H)の値(bits 47-32)が0x10ですので、syscall実行時にCSレジスタには0x10がロードされ、SSレジスタには0x10+0x8=0x18がロードされることがわかります。64-bit modeではLSTAR MSR(MSR-C000_0082H)の値0xfffff80001024040がRIPレジスタにロードされ、Compatibility modeではCSTAR MSR(MSR-C000_0083H)の値0xfffff80001023d80がRIPレジスタにロードされることがわかります。(ただしこれらのポインタで実際に意味を持つのは下位48ビットのみです。

AMD64/EM64Tでは仮想アドレス空間が(現時点では)48ビットに制限されていることに注意してください。この場合上位16ビット(bits 63-48)は、有効なビット(bits 47-0)の最上位ビット(bit 47, MSB(Most Significant Bit)と呼ぶ)の値で埋めておきます。これをCanonical Address Formと呼びます。ポインタとして有効なのは下位48ビットのみです。)さらにRFLAGSとはSYSCALL_FLAG_MASK(MSR-C000_0084H)の値(bits 31-0)である0x14700と
ANDをとることがわかります。これはsyscall実行時のフラグのうちRF, NT, DF, IF, TFフラグがカーネルモード実行時も引き継がれることを意味します。

セレクタについて調べます。

セレクタ0x10はコードセグメントであることがわかります。ベースアドレスやリミット値は64-bit modeでは無視されるので、このセレクタで仮想アドレス空間の全領域をアクセスできることになります。L bit(LONG)が立っているので、このコードセグメントがロングモード(64-bit mode もしくはcompatibility mode)で実行されることもわかります。Pl(=DPL)がゼロなので、カーネルモードのコードセグメントであることもわかります。

セレクタ0x18はデータセグメントであることがわかります。ベースアドレスやリミット値は64-bit modeでは無視されるので、このセレクタで仮想アドレス空間の全領域をアクセスできることになります。(ベースアドレス、リミット値、その他のフラグがセットされているのはcompatibility modeでも共有するセレクタだからなのでしょうか?)

起草日:2005年1月9日(日)(www.marbacka.net内の別のサイトで公開)
最終更新日:2017年2月19日(日)

スポンサーリンク