vpmxfer という VCPI を使って PCI の MMIO にアクセスするために作ったライブラリを配布しているが、DOS でプロテクトモードを取り扱う規格には DPMI もある。 PCI の MMIO にアクセスするという目的は VCPI で達成できているわけだが、これまで DPMI を使ったプログラムの作成経験はなく興味があったので、適当に情報を集めて処理を書いてみた。

参考文献は以下の辺り。

  • トラ技コンピュータ 1993年10月号 特集 保存版 DPMI によるメモリ管理のすべて
  • IF セレクションシリーズ 実践的プロテクト・モード研究
  • MS-DOS基本プログラミング集 6 X86 プロテクトモード・プログラミング
  • DPMI specification version 0.9、同 1.0

サンプルプログラム

サンプルプログラムは dpmimmio.asm からダウンロードできます。 このファイルは PDS とします。 プログラムの内容は、y7setup の処理を引っ張ってきて、PCI バス上で最初に見つかった YAMAHA YMF724/744/754 の MMIO の値を読むものになっています。

予備知識

  • DPMI ホストは特権レベル0で、DPMI クライアントは1以上の特権レベルで動く。
  • DPMI クライアントがホストを呼び出すときに使う割込は int 2fhint 31h の2つがある。
    • int 2fh にはリアルモードのみ、またはリアルモードとプロテクトモードの両方から呼び出す機能が定義されている。
    • int 31h にはプロテクトモードからのみ呼び出せる機能が定義されている。
    • 呼び出しが正常に終了したかは CF に入っており、CF=0 で成功、CF=1 で失敗である。また、DPMI 1.0 では AX にエラーコードが入る。
  • DPMI クライアントは GDT は操作できないが自らの LDT を操作できる。

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

まず、プロテクトモードへ移行する前に DPMI ホストの存在を int 2fh (AX=1687h) でチェックする必要がある。 DPMI ホストが存在すれば AX=0000h が、その他のレジスタに以下のような DPMI ホストの情報が返る。

  • BX:フラグ
    • LSB が1なら DPMI ホストの 32bit サポートあり。
  • CL:CPU の種類 (02hで286、03hで386、04hで486)
  • DX:DPMI ホストのバージョン(DH がメジャー、DL がマイナーバージョン)
  • SI:DPMI が内部で使用するメモリ領域(リアルモード用)のパラグラフ数
  • ES:DI:プロテクトモード移行ルーチンがあるメモリアドレス

以下の値を設定して int 2fh (AX=1687h) の結果 ES:DI に得られたアドレスを far コールするとプロテクトモードに移行する。

  • AX:フラグ
    • 32bit アプリケーションなら LSB を1にする(ホストの 32bit サポートがある場合に指定可能)。
  • ESint 2fh (AX=1687h) の結果 SI に得られたサイズで確保したメモリのセグメントアドレス
    • このメモリ確保はクライアントの仕事である。また、SI=0 だった場合 ES は使用されない。

移行に失敗すると、リアルモードのまま CF=1 で戻ってくる。 成功すると、CF=0 で戻ってきてその時点でプロテクトモードになっており、また、以下のレジスタが設定されており、他のレジスタは移行前の値を保持している。

  • CS:リアルモード時の CS と同じベースでリミットが 64KiB - 1 の 16bit コードセグメントを指すセレクタ
  • DS:リアルモード時の DS と同じベースでリミットが 64KiB - 1 の 16bit データセグメントを指すセレクタ
  • ES:クライアントの PSP の位置がベースでリミットが 255 のデータセグメントを指すセレクタ
  • SS:リアルモード時の SS と同じベースでリミットが 64KiB - 1 の 16bit データセグメントを指すセレクタ
  • FS:0 になっている
  • GS:0 になっている

また、32bit アプリケーションとして指定した場合、以下のような挙動となる。

  • 移行直後のプログラムは 16bit コードセグメントで実行されるが、データとスタックのセグメントディスクリプタの D/B ビットは1となる。
  • プロテクトモード移行時のレジスタ値保存の例外として ESP の上位 16bit は0でクリアされる。
  • ポインタを取り扱う DPMI 関数では 32bit 長の値を使用する。

プログラムの終了

プロテクトモード中でも int 21h (AH=4ch) を呼んでプログラムを終了することができる。 今回は、プロテクトモード移行後にリアルモードに戻る必要がないので、この方法で直接プログラムを終了している。

PCI MMIO のリニアアドレスへのマッピング

MMIO の物理アドレスを DPMI クライアントのリニアアドレスに割り当てるには、以下の入力を受け取る int 31h (AX=0800h) のファンクションを使う。 割り当てに成功すると、与えた物理アドレスが割り当てられたリニアアドレスが BX:CX に返る。 ただし、このファンクションは割り当てまでしか行わないため、追加で LDT の設定も行う必要がある。

  • AX:0800h
  • BX:CX:割り当てる物理アドレス
  • SI:DI:割り当てる範囲のサイズ(バイト単位)

DPMI 0.9 には割り当てを解放するファンクションが定義されていないが、1.0 では int 31h (AX=0801h) が定義されており、以下のパラメータで呼び出せる。

  • AX:0801h
  • BX:CX:解放するリニアアドレス(int 31h (AX=0800h) で得たもの)

LDT 設定

前述の通り int 31h (AX=0800h) で MMIO を割り当てたリニアアドレスにアクセスするためのディスクリプタを LDT に追加する必要がある。 今回は int 31h (AX=0000h) で LDT へのディスクリプタ追加を行い、 int 31h (AX=000ch) でディスクリプタの内容の編集を行っている。

  • int 31h (AX=0000h) :ディスクリプタの追加
    • 入力
      • AX:0000h
      • CX:追加するディスクリプタの個数(今回は1)
    • 戻値
      • AX:追加されたディスクリプタの最初の1個のセレクタ
  • int 31h (AX=000bc):ディスクリプタの内容の設定
    • 入力
      • AX:000ch
      • BX:設定するディスクリプタのセレクタ
      • ES:(E)DI:設定するディスクリプタの内容が書かれたメモリ領域のアドレス

ディスクリプタの値を書くメモリアドレスの表記が ES:(E)DI となっているのは、前述のプロテクトモード移行時のフラグの値で 32bit アプリケーションと指定したかどうかによって、ポインタのアドレス長が 32bit か 16bit かで変わるためである。

ディスクリプタの設定を行う際に、権限の設定が間違っていると LDT への設定が行われない。 権限の取得は lar 命令で行うことができる。 今回は lar 命令で取得した DS の指すディスクリプタのアクセス権限を使い回して PCI MMIO のディスクリプタの値を準備している。

LDT が設定できれば、適当なリニアアドレスに対する mov などで PCI MMIO へアクセスできる筈である。

おわりに

今回作成した DPMI を利用した PCI MMIO アクセスプログラムは実稼働2~3日で完成してしまった。 VCPI を使った vpmxfer の実装の際は、(本格的なプロテクトモードプログラミングが初めてだったというのもあるが)トリプルフォルト地獄で時間がかかった。 そのため、幾らか身構えていたのだが正直拍子抜けである。 VCPI は全部できる一方で全部やらなくてはならないので、プロテクトモードで特別なことをする必要がないのであれば DPMI を使うのが楽で良いと思う。