ホーム > プログラミングメモ > 仮想86モード拡張(VME)考察
 

仮想86モード拡張(VME)考察

Pentium以降のCPUで導入された仮想86モード拡張(仮想8086モード拡張)について考えてみます。 仮想86モード(仮想8086モード)そのものは386で導入されたものですが、 この機能拡張によりマルチタスクOS環境下でのMS-DOSプログラムの実行速度が向上します。 (ロングモードには仮想86モードがありませんので、当ページの記述は32ビット環境限定です。)

仮想86モードの概要

仮想86モードとは、32ビットプロテクトモード下で動作するタスクの一形態です。 主として8086用(16ビット・リアルモード用)に作られたプログラム(MS-DOSプログラムなど)を 32ビット・マルチタスクOSの環境下で動作させることを目的としています。

一番の特徴は、アドレス(リニアアドレス)がリアルモードと同じように生成されることです。 16ビットもしくは32ビット・プロテクトモードのプログラムであれば、 セグメントレジスタに入っている値はセレクタとして用いられ、そのセレクタが指すセグメント・ディスクリプタに記述された ベースアドレスはオフセットアドレス(命令のインストラクション・ポインタやオペランドアドレスなど。正確には実効アドレスと呼ぶ)と加算されて リニアアドレスとして用いられます。 一方仮想86モードになると、セグメントレジスタに入っている値はセレクタとして用いられるのではなく、 その値が直接リニアアドレスの生成に用いられます。すなわち8086と同じように、 セグメントレジスタに入っている値が16倍されたもの(セグメントアドレス)に、オフセットアドレスが加算されて リニアアドレスとして用いられます。

仮想86モードはプロテクトモード下で動作しますので、生成されたリニアアドレスは (ページングが有効であれば)ページングの対象となります。

仮想86モードで注意の必要な命令

仮想86モードはCPL=3で動作し、特権命令など実行できない命令があります。 特に重要なのは、リアルモードでは問題にならないものの 仮想86モードでは問題になる命令があることです。

【仮想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を実行すると 以下のようになります。

【仮想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)が変更されることはありません。 おおよそこんな感じになります。

【仮想86モードでの割り込みフラグのエミュレート(抜粋)】
(1)CLI実行時のエミュレート IF=0の状態(割り込みフラグのマスク)を模擬する。(仮想割り込みフラグ=0) (2)STI実行時のエミュレート IF=1の状態(割り込みフラグのアンマスク)を模擬する。(仮想割り込みフラグ=1) 処理が保留になっている外部割込みがあれば、 その処理を開始させる。

外部割込み発生時の対応は以下のようになります。 上記の「仮想割り込みフラグ」の状態によって 処理が変わってくる点に注意してください。

【仮想86モードでの外部割り込みの処理(抜粋)】
(1)外部割込み発生時の対応 外部割込みを処理するべきタスクが仮想86モードのタスクの場合 そのタスクの仮想割り込みフラグがクリア(=0)されているのであれば その処理を保留しておく。 仮想割り込みフラグがセット(=1)されているのであれば その処理を開始させる。

OSが割り込みフラグの操作をエミュレートすることで、 OSの信頼性、OSとの整合性を損なう恐れはなくなりますが、 その代償として、CLI/STI命令の実行がかなり遅くなってしまいます。 命令を実行する毎に一般保護例外になって例外ハンドラに制御がうつるためです。 CLI/STIを多用するMS-DOSプログラムで特に問題になります。

仮想86モード拡張での割り込みフラグ操作

仮想86モード拡張では、上記割り込みフラグのエミュレートの一部を CPU側で代行します。 これにより、割り込みフラグを操作する命令の実行が高速になります。

【仮想86モード拡張でのCLI/STI】
(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は例外ハンドラの中で保留中の外部割込みの処理を開始させます。

【仮想86モード拡張での外部割り込みの処理(抜粋)】
(1)外部割込み発生時の対応 外部割込みを処理するべきタスクが仮想86モードのタスクの場合 そのタスクの仮想割り込みフラグがクリア(VIF=0)されているのであれば VIP=1にする。 仮想割り込みフラグがセット(VIF=1)されているのであれば その処理を開始させる。(VIP=0にする。)


速度比較

仮想86モード拡張を使うと、標準の仮想86モードと比べてどの程度速くなるのでしょうか? 簡単なプログラムでテストしてみました。 テストに用いたプログラムです。

【clisti.c】
/* * 仮想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で実行してみました。

【CLI/STIを1億回実行するのに必要な時間】
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)と考えるのが妥当)

【WindowsNT 4.0 Workstation SP1でのCR4】
kd> R cr4 cr4=00000011 kd> R iopl iopl=0


【Windows2000 SP4でのCR4】
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」キーを押してエディタ画面に入ります。そして下記プログラムを打ち込みます。

【アセンブリ言語(FX-890P)】
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」を開く)

【BASIC言語メニュー】
P 0 1 2 3 4 5 6 7 8 9 50768B Ready P0

上記画面から直接下記プログラムを入力します。 (C言語でなくBASIC言語を使うのは、時間計測の関数がC言語には用意されていないからです。)

【BASIC言語(FX-890P)】
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コマンドを実行します。

【FX-890Pでの実行結果】
Ready P0 RUN TIME(SEC)= 734.3 Ready P0

734秒、MMX Pentium(300MHz)のおよそ100倍の所要時間になりました。 意外に速いと感じられるのではないでしょうか?

関連リンク

http://www.rcollins.org/ddj/ddj.html
http://www.x86.org/articles/articles.htm
http://www.x86.org/secrets/intelsecrets.htm
http://www.x86.org/errata/errataseries.htm
http://www.x86.org/ddj/ddj.htm
http://www.x86.org/intel.doc/inteldocs.htm

参考文献

最終更新日:2005年1月24日(月)

Copyright(C) 毛流麦花
 
64ビットCPU(AMD64+EM64T)でアセンブラ