Pentium以降のCPUで導入された仮想86モード拡張(仮想8086モード拡張)について考えてみます。仮想86モード(仮想8086モード)そのものは386で導入されたものですが、この機能拡張によりマルチタスクOS環境下でのMS-DOSプログラムの実行速度が向上します。(ロングモードには仮想86モードがありませんので、当ページの記述は32ビット環境限定です。)
(本記事の初稿は2005年です。64ビットCPUが普及した現在でも有用な内容と思われるので、そのままの内容で公開します。)
■仮想86モードの概要
仮想86モードとは、32ビットプロテクトモード下で動作するタスクの一形態です。主として8086用(16ビット・リアルモード用)に作られたプログラム(MS-DOSプログラムなど)を32ビット・マルチタスクOSの環境下で動作させることを目的としています。
一番の特徴は、アドレス(リニアアドレス)がリアルモードと同じように生成されることです。16ビットもしくは32ビット・プロテクトモードのプログラムであれば、セグメントレジスタに入っている値はセレクタとして用いられ、そのセレクタが指すセグメント・ディスクリプタに記述されたベースアドレスはオフセットアドレス(命令のインストラクション・ポインタやオペランドアドレスなど。正確には実効アドレスと呼ぶ)と加算されてリニアアドレスとして用いられます。一方仮想86モードになると、セグメントレジスタに入っている値はセレクタとして用いられるのではなく、その値が直接リニアアドレスの生成に用いられます。すなわち8086と同じように、セグメントレジスタに入っている値が16倍されたもの(セグメントアドレス)に、オフセットアドレスが加算されてリニアアドレスとして用いられます。
仮想86モードはプロテクトモード下で動作しますので、生成されたリニアアドレスは(ページングが有効であれば)ページングの対象となります。
■仮想86モードで注意の必要な命令
仮想86モードはCPL=3で動作し、特権命令など実行できない命令があります。特に重要なのは、リアルモードでは問題にならないものの仮想86モードでは問題になる命令があることです。
以下の命令はリアルモードでは問題にならないものの、 仮想86モードでは注意が必要になる。 (1)I/Oポートの入出力 IN, OUT, INS, OUTS (2)割り込みフラグ(IF)の操作 CLI, STI, PUSHF, POPF, INT, IRET
これらの命令が特別な扱いになっているのは、アプリケーションプログラムから勝手に実行されるとOSの信頼性、OSとの整合性を損なう恐れがあるためです。
(1)については、TSS中のI/O許可ビットマップでコントロールします。クリア(=0)されていればI/Oポートへのアクセスが許可され、セット(=1)されているとI/Oポートへアクセスしようとすると一般保護例外が発生します。(2)が本ページのメインテーマになります。
■仮想86モードでの割り込みフラグ操作
仮想86モードでの割り込みフラグ操作はIOPL(I/O特権レベル)によって動作が異なります。なおIOPLによって動作が異なる命令をIOPLセンシティブな命令と呼びます。仮想86モードで動作しているプログラムがCLI/STIを実行すると以下のようになります。
(1)IOPL=3の場合 (CPL=3のプログラムでも割り込みフラグ(IF)を操作できる状態) CLI: IF=0になる。 STI: IF=1になる。 (2)IOPL=0の場合(正確にはIOPL<3の場合) (CPL=3のプログラムからの割り込みフラグ(IF)操作を認めない状態) CLI: 一般保護例外になる。 STI: 一般保護例外になる。
IOPL=0の場合はCLI/STIを実行すると一般保護例外になりますのでOSは例外ハンドラの中で割り込みフラグのエミュレートを行います。本物の割り込みフラグ(IF)が変更されることはありません。おおよそこんな感じになります。
(1)CLI実行時のエミュレート IF=0の状態(割り込みフラグのマスク)を模擬する。(仮想割り込みフラグ=0) (2)STI実行時のエミュレート IF=1の状態(割り込みフラグのアンマスク)を模擬する。(仮想割り込みフラグ=1) 処理が保留になっている外部割込みがあれば、 その処理を開始させる。
外部割込み発生時の対応は以下のようになります。上記の「仮想割り込みフラグ」の状態によって処理が変わってくる点に注意してください。
(1)外部割込み発生時の対応 外部割込みを処理するべきタスクが仮想86モードのタスクの場合 そのタスクの仮想割り込みフラグがクリア(=0)されているのであれば その処理を保留しておく。 仮想割り込みフラグがセット(=1)されているのであれば その処理を開始させる。
OSが割り込みフラグの操作をエミュレートすることで、OSの信頼性、OSとの整合性を損なう恐れはなくなりますが、
その代償として、CLI/STI命令の実行がかなり遅くなってしまいます。命令を実行する毎に一般保護例外になって例外ハンドラに制御がうつるためです。CLI/STIを多用するMS-DOSプログラムで特に問題になります。
■仮想86モード拡張での割り込みフラグ操作
仮想86モード拡張では、上記割り込みフラグのエミュレートの一部をCPU側で代行します。これにより、割り込みフラグを操作する命令の実行が高速になります。
(1)IOPL=3の場合 <- 標準の仮想86モードの場合と同じ (CPL=3のプログラムでも割り込みフラグ(IF)を操作できる状態) CLI: IF=0になる。 STI: IF=1になる。 (2)IOPL=0の場合(正確にはIOPL<3の場合) (CPL=3のプログラムからの割り込みフラグ(IF)操作を認めない状態) CLI: VIF=0になる。 STI: VIP=0の場合はVIF=1になる。 VIP=1の場合は一般保護例外になる。
IOPL=3の場合は標準の仮想86モードと変わりありません、IOPL=0の場合の動作が異なっています。
仮想86モード拡張では、EFLAGSレジスタ中の2つのビットが有効になります。VIF(Virtual Interrupt Flag, bit19)とVIP(Virtual Interrupt Pending, bit20)です。CLI/STIを実行するとIFではなくVIFを操作することになります。VIPは保留中の外部割込みがあるかどうかを示すフラグでOSがセット/クリアします。
これら2つのフラグによって、割り込みフラグのエミュレートのほとんどがCPUによって直接行われることになります。
VIP=1は保留中の外部割込みがあることを示し、VIP=1の時にSTIを実行すると一般保護例外になるのでOSは例外ハンドラの中で保留中の外部割込みの処理を開始させます。
(1)外部割込み発生時の対応 外部割込みを処理するべきタスクが仮想86モードのタスクの場合 そのタスクの仮想割り込みフラグがクリア(VIF=0)されているのであれば VIP=1にする。 仮想割り込みフラグがセット(VIF=1)されているのであれば その処理を開始させる。(VIP=0にする。)
■速度比較
仮想86モード拡張を使うと、標準の仮想86モードと比べてどの程度速くなるのでしょうか?簡単なプログラムでテストしてみました。テストに用いたプログラムです。
/* * 仮想86モード拡張(VME)の効果を確かめるためのプログラム * CLI/STIを1億回実行し、所要時間を調べる。 * * MS-DOS用のプログラム * LSI C-86(試食版)でのコンパイルを想定 */ #include <stdio.h> #include <time.h> #define cli() _asm_c("\n\tCLI") #define sti() _asm_c("\n\tSTI") #define COUNTS 100000000 void main(void){ time_t t1,t2; unsigned long i; double et; i=COUNTS; time(&t1); while(i--) { cli(); sti(); } time(&t2); et=difftime(t2,t1); printf("CLI/STI operation(%lu times) required %.2f seconds.\n", COUNTS, et); } /* END of main() */
CLI/STIを1億回実行し、所要時間を調べます。CLI/STIはインラインアセンブラで記述しています。上記プログラムをLSI C-86でコンパイルし、さまざまなOSで実行してみました。
CPUはすべてMMX Pentium 300MHz(P55C)を使用。 所要時間は10回実行した結果の平均。 ■リアルモード (1)MS-DOS 6.2 7.8秒 ■仮想86モード(IOPL=3) (2)MS-DOS 6.2+EMM386(※) 7.6秒 ■仮想86モード(IOPL=0, VME=0) (3)WindowsNT 3.51 Workstation SP3 355秒 ■仮想86モード拡張(IOPL=0, VME=1) (4)WindowsNT 4.0 Workstation SP1 8.5秒 (5)Windows2000 SP4 8.5秒 ※EMM386とは、仮想86モードを利用した EMSメモリ(Expanded Memory Specification)マネージャーのこと。
時間の計測精度が1秒単位ですので、1秒未満の値にはあまり意味がありません。(3)と(4)(5)を比べると、仮想86モード拡張の効果が大きいことがわかります。さらに(1)と(4)(5)を比べると、仮想86モード拡張ではリアルモード並みの速度が期待できることもわかります。実際のアプリケーションプログラムでは外部割り込みも多数発生するでしょうし、CLI/STIばかり実行しているわけではないのでこんなに速くなることはないでしょうが、それでも仮想86モード拡張の効果は抜群です。
(4)(5)のVME=1(仮想86モード拡張が有効)であることはKD(カーネルデバッガ)を用いて
以下のようにすることで確認できます。なお(3)についてはなぜかカーネルデバッガが使えなかったので確認していません。すなわちNT3.51のVME=0は推測です。(NT3.51の登場時期を考慮するとVME=0(ターゲットとするCPUは386/486)と考えるのが妥当)
kd> R cr4 cr4=00000011 kd> R iopl iopl=0
kd> R cr4 cr4=00000011 kd> R iopl iopl=0
CR4レジスタの最下位ビット(bit0)がVMEビット(Virtual-8086 Mode Extensions, 仮想86モード拡張Enable/Disableするためのビット)ですので、WindowsNT4.0もWindows2000も仮想86モード拡張を使っていることがわかります。(Win2KはともかくNT4.0が仮想86モード拡張を使っていることは今回初めて知った。)なおCR4レジスタはPentiumで追加された制御レジスタです。
■ポケコンでも実行
番外編として、上記と同様のプログラムをポケコンでも走らせてみることにしました。用いたのはCASIO製FX-890Pです。このマシンはポケコンながら8086互換の16ビットCPUである80L188EB(組み込み用途向けCPU)を搭載しています。
クロック周波数は仕様表に載っていませんが、インテルのカタログを見ると3V動作品は13MHz, 16MHz, 5V動作品は13MHz, 20MHz, 25MHzがラインナップされているので、おそらく13MHz, 16MHzのいずれかと思われます。(もしかしたら、さらに低クロックで動作させているのかもしれません。)
「ポケコンなんてまだ売られているの?」とお考えになるかもしれません。購入したのは2004年の春頃ですが、しっかり売られていました。店員に質問すると「工業高校などで需要があるので、メーカーはまだ生産を続けている」のだそうです。カシオだけでなく、シャープも同様だそうです。(購入店は秋葉原ラジオセンター内にある「つかさ無線」です。)価格は2~3万円弱で、最近のこの手の製品にしては珍しく「MADE IN JAPAN」でした。
このFX-890Pの良いところは、8086互換のCPUを搭載していることです。ポケコンというとZ80互換のCPUを搭載している場合が多いですが、こちらは8086互換なので、パソコン感覚でプログラミングできます。さらに良いのは、アセンブラ、BASIC、Cが使えることです。(CASLも使えます。)小さなプログラムを作って遊ぶにはうってつけです。
まずメインメニューを出します。(電源ON -> 「MENU」キー)
< MENU > 1:F.COM 2:BASIC 3:C 4:CASL 5:ASMBL 6:FX 7:MODE
次にアセンブリ言語メニューを出します。(「5」を押して「5:ASMBL」を開く)
< ASMBL > F 0 1 2 3 4 5 6 7 8 9 50768B F0>Assemble/Source/List
「S」キーを押してエディタ画面に入ります。そして下記プログラムを打ち込みます。
ORG 2000H MOV DX, 10000 LOOP1: MOV CX, 10000 LOOP2: CLI STI DEC CX JNZ LOOP2 DEC DX JNZ LOOP1 IRET
CLI/STIを1億回実行して戻るだけのプログラムです。プログラムの最後がRETではなくIRETになっていますが、これはBASICやCからサブルーチン・コールした場合の戻り方がこうなっていることによります。
入力し終えたら、「SUB MENU」(「SHIFT」+「MENU」)を押して「アセンブリ言語メニュー」に戻り、「A」キーを押してアセンブルを開始します。「Assemble Start!」と表示された後、以下のような画面になるはずです。
Assemble End! End Address = 2010H Total Error = 0
次にBASIC言語メニューを出します。(「MENU」キー -> 「2」を押して「2:BASIC」を開く)
P 0 1 2 3 4 5 6 7 8 9 50768B Ready P0
上記画面から直接下記プログラムを入力します。(C言語でなくBASIC言語を使うのは、時間計測の関数がC言語には用意されていないからです。)
10 CLEAR , &H100 20 TIMER=0 30 T1=TIMER 40 CALL &H2000 50 T2=TIMER 60 T3=(T2-T1)/10 70 PRINT "TIME(SEC)=";T3
行番号20において、タイマー変数(システム変数)を初期化します。1/10秒毎にカウントアップする16ビット長の変数です。行番号40において、さきほど入力したアセンブリ言語のルーチンを呼び出します。ちなみにFX-890Pでは、セグメントベース(CSレジスタの値)はデフォルトでゼロになっていますので、リニアアドレスは02000Hになります。
入力し終えたら、さっそく実行しましょう。BASICのRUNコマンドを実行します。
Ready P0 RUN TIME(SEC)= 734.3 Ready P0
734秒、MMX Pentium(300MHz)のおよそ100倍の所要時間になりました。意外に速いと感じられるのではないでしょうか?
起草日:2005年1月24日(月)(www.marbacka.net内の別のサイトで公開)
最終更新日:2017年2月19日(日)