VCPI メモ 第1回: 最低限の使い方を思い出す

タグ: #asm  #DOS 

vpmxfer を作った時に VCPI を使ったものの詳細を忘れてしまったので色々調べ直すことにしました。 手始めにプロテクトモードに行って帰ってくるのに最低限程度のプログラムを作ります。

VCPI を使ってプロテクトモードに移行する場合、自力で行う必要がある事柄が大分多いです。 そのため、x86 のプロテクトモードでのセグメント管理やページング等について 事前に知識を入手しておいた方がとっつきやすいと思います。

「VCPI メモ」シリーズの記事は主に以下の資料を参考に作成しています。

  • CQ出版 トラ技コンピュータ 1992年10月号 特集 MS-DOS からのプロテクト・モード活用法
  • CQ出版 MS-DOS基本プログラミング集 6 X86プロテクトモード・プログラミング
  • CQ出版 別冊インターフェース 実践的プロテクト・モード研究
  • ZOB.Club The Advanced Assembler 386 上ノ巻

サンプルプログラム

VCPI を使ってプロテクトモードと V86 モードを行き来するサンプルプログラムのコードとアセンブル用のバッチファイルを以下のリンクからダウンロードできます。 以下4つのファイルのコードは PDS とします。適宜ご活用ください。

assemble.bat では、JWasmJWlink でアセンブルとリンクをそれぞれ行うようにしています。 これらのプログラムを実行できる環境で、サンプルプログラムのコードと assemble.bat を同じディレクトリに置いて assemble.bat を実行してください。 この方法は次回以降のサンプルコードについても同様です。 また、 procs.incstructs.inc も次回以降のサンプルコードでインクルードして使います。

実行結果例

VCPI が使える環境でサンプルプログラムを実行すると以下のような内容が表示されます。 プログラム中での各種チェックや動作のメッセージと、test_val の値と、プロテクトモードで保存した EFLAGS の値を表示しています(test_val はプロテクトモードに移行した後、動作確認でビット反転させている値です)。 VCPI が使えないなどの場合は、その旨を表示してそこでプログラムが終了します。

A:\> vcpi_01.exe
VCPI test program
EMS driver available
VCPI available
VCPI initialization OK
test_val: 55
Go to protect mode
Returned to V86 mode
test_val: aa
FEDCBA9866543210FEDCBA9866543210
                  II
           VV     OO
          IIIAVR NPPODITSZ A P C
          DPFCMF TLLFFFFFF F F F
00000000000000000011000010000010
Finished

VCPI の存在確認

VCPI が使えるか確認する前に EMS ドライバが存在しているか確認する必要があります。

EMS ドライバが存在する場合 int 67h (AX=0de00h) を呼び出します。 VCPI が使える場合は AH に0が、BX に VCPI のバージョンがそれぞれ返ります。 バージョンは 1.00 なら BX=0100h というような値となります。 VCPI が使えない場合は AH に非0の値が返ります。

VCPI の初期化

以下の領域を事前に用意しておく必要があります。

  • ページディレクトリ、ページテーブル
  • GTD
  • TSS

ページテーブルの用意

ページディレクトリが1個と、ページテーブルが最低1個必要です。 ページディレクトリとページテーブルともに 4KiB アライメントされている必要があります。

ページテーブルのうち1個は、ページディレクトリ中の最低位 4MiB (0~3fffffh)を担当するエントリが指すページテーブルとなります。 このページテーブルの(下位側の)初期化は VPCI が行います。

ここでは、簡単のため、下記のように必要なページディレクトリとページテーブルの個数より1個多い4KiB 単位の連続したメモリを確保しておき、その中でアライメントが 4KiB となる位置を探して使うことにします。

pagetbl_start	label byte
	org	(N + 1) * 4 * 1024	; N=必要なページディレクトリとページテーブルの個数

TSS の用意

以下のように TSS と IO 番地ビットマスクを連続して確保しておき、TSS の I/O Map Base Address に 0068h を入れておくのが簡単だと思います。 また、プロテクトモードのコードをすべて CPL <= IOPL を満たしながら実行する場合は I/O 番地ビットマスクは不要になります。

+----------------------+ <- 0000h
| TSS                  |
+----------------------+ <- 0068h
| I/O 番地ビットマスク |
+----------------------+

GDT の用意

例えば以下のような GDT を用意すれば良いです。

用途limitbaseアクセスバイト備考
NULL0000h
TSSTSS の大きさTSS 先頭89h
16bit コード64KiBCS 先頭9ah
データ64KiBDS 先頭92h
スタック64KiBSS 先頭92h
V86 モード移行時用後述092h移行時に DS に設定
VCPI コード---VCPI サーバが設定
VCPI データ0---同上
VCPI データ1---同上

「V86 モード移行時用」のディスクリプタの説明は後述します。

VCPI 初期化処理の呼び出し

以下の引数を設定して int 67h (AX=0de01h) を呼び出します。

  • ES:DI: ページテーブルのアドレス
  • DS:SI: GDT 内の VCPI 用の3個連続したエントリの先頭アドレス

成功すると以下の値が戻ります。

  • AH: 00h
  • DI: 未初期化のページテーブルエントリのオフセット
    • DI 以降のエントリはクライアント側で好きな値を入れることができます
  • EBX: プロテクトモード用の VPCI サーバのエントリポイントのオフセット
    • このオフセットが含まれるのは VCPI コードのセグメントです

最下位 1MiB の範囲のメモリのマッピングは VCPI サーバとクライアントで同一になります。

ページテーブルの準備

ページディレクトリ中の最低位 4MiB (0~3fffffh)を担当するエントリが、VCPI に初期化させたページテーブルを指すようにします。

追加でページングを行ってアクセスしたいメモリ領域があれば、ページディレクトリとページテーブルの追加の設定を適宜行います。

プロテクトモードへの移行

以下の作業を行い int 67h (AX=0de0ch) を呼び出します。

  • 割込を禁止する
  • 以下のレジスタを設定する
    • ESI: 下記の値を設定したメモリ領域のリニアアドレス (0fffdeh 以下のアドレス)

ESI に設定するアドレスのメモリ領域の構造は以下の通りです。 GDTR の値と IDTR の値は最低位 1MiB の範囲に配置しておく必要があります。

31          15          0
+-----------+-----------+ <- +00h (ESI)
|              CR3 の値 |
+-----------+-----------+ <- +04h
|   GDTR の値のアドレス |
+-----------+-----------+ <- +08h
|   IDTR の値のアドレス |
+-----------+-----------+ <- +0ch
|   TR の値 | LDTR の値 |
+-----------+-----------+ <- +10h
|             EIP の値  |
+-----------+-----------+ <- +14h
            |   CS の値 |
            +-----------+

プロテクトモードに移行した時には以下の状態になっています。

  • ESI に設定した各レジスタの値が対応するレジスタに設定されている
  • ESI に設定したメモリ領域の CS:EIP の位置の命令を実行している
  • 割込は禁止されている
  • EAX, ESI, DS, ES, FS, GS の値は不定になっている

V86 モードへの移行

以下の作業を行い、 AX=0de0ch でプロテクトモード用の VCPI サーバのエントリポイントを far call します。

  • 割込を禁止する
  • 以下のレジスタを設定する
    • DS: ベースアドレスが0で VCPI サーバが初期化時にページテーブルに登録したメモリ領域にアクセス可能なディスクリプタを示すセレクタ
    • SS:ESP: 下記の値が積まれたスタック領域

DS に設定するセレクタが示すディスクリプタの limit は、ページテーブルのうち VCPI サーバが初期化した範囲を走査して最も大きいアドレスを見つけることで計算できます。 ただし、limit が大きい分には問題無いようなので limit を 4GiB 固定にしてしまって計算を省くことも可能です。 今回の例では簡単のために limit は 4GiB 固定としています。

SS:ESP が指すスタック領域の構造は以下の通りです。

31         15         0
+----------+----------+ <- +00h (SS:ESP)
|            EIP の値 |
+----------+----------+ <- +04h
|   未使用 |  CS の値 |
+----------+----------+ <- +08h
| EFLAGS のために予約 |
+----------+----------+ <- +0ch
|            ESP の値 |
+----------+----------+ <- +10h
|   未使用 |  SS の値 |
+----------+----------+ <- +14h
|   未使用 |  ES の値 |
+----------+----------+ <- +18h
|   未使用 |  DS の値 |
+----------+----------+ <- +1ch
|   未使用 |  FS の値 |
+----------+----------+ <- +20h
|   未使用 |  GS の値 |
+----------+----------+

V86 モードに移行した時には以下の状態になっています。

  • スタックに積んだ ES:EIP の位置の命令を実行している
  • 割込は禁止されている
  • ESP, SS, ES, DS, FS, GS はスタックに積んだ値が設定されている
  • GDTR, IDTR, LDTR, TR は VCPI サーバの値が設定されている

デバッグに関する補足

実機環境では、何かの間違いでリセットがかかってしまうなど、デバッグが難しいです。 そのため、実行中のプログラムをデバッグ可能なエミュレータ(QEMU 等)を使って開発を行った方が良いかもしれません。

次回予告

第2回はサンプルプログラムの改良をやります。


一覧に戻る