はじめてのDPMI

「いまどき使う?」の感もありますが、WindowsXP上でDPMI(DOS Protected Mode Interfaceを使ったDOSプログラムを作成・アセンブル・実行してみます。DOS上で手軽に32ビットプロテクトモードのプログラムを実行できることがわかります。

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

なお本稿ではMS-DOS用のアセンブラーとリンカーが必要になります。ここではBorland社製のTurbo Assembler(TASM)とTurbo Link(TLINK)を使います。もしもこれらを持っていない場合は、フリーウェアのアセンブラー・リンカーを入手してください。

■DPMIとは

DPMI(DOS Protected Mode Interface)とは、16ビット(リアルモードもしくは仮想86モード)のOSであるMS-DOS上でプロテクトモードのプログラムを実行させるためのインターフェースを提供するためのものです。以下のようなことができます。

  • CPUモードの切り替え(リアルモード(仮想86モード)とプロテクトモード)
  • LDTディスクリプタの管理
  • 割り込み(ハードウェア割り込み, ソフトウェア割り込み), 例外の管理
  • 仮想記憶, ページング関係のサービス
  • メモリ管理

DPMIでは80286などの16ビットプロテクトモードでプログラムを実行させることもできるようになっていますが、メインはやはり32ビットプロテクトモードです。仮想記憶、ページング関係の機能は当然ながら80386以上でないと使うことができません。

DPMIのプログラム(DPMIクライアント)はCPUのリング3で動作し、DPMIホスト(DPMIサーバー)はリング0で動作します。

DPMIが使える環境として最初に登場したのは、Windows3.0でした。(これ以前にWindows386(Windows/386)がありましたね。なつかしい。)Windows3.0にはリアルモード、スタンダードモード、386エンハンストモードがあり、このうちの386エンハンストモードのMS-DOSプロンプトでDPMIを使うことができました。NECのPC-9800シリーズ用のMS-DOS ver 5.0Aでは単体のDPMIホストが付属しており、これはよく使いました。(IO・DATAから販売されていたMEMORY SERVERにもDPMIホストが付属していたものの、DOS 5.0A付属のDPMIとはえらく使い勝手が異なり、ほとんど使わなかった。)

■アセンブルから実行まで

さっそく試してみましょう。全部で3ファイルあります。main.asm, switch.asm, io.asm です。main.asmはメインプログラムで、32ビットプロテクトモード上でやらせたいことを記述します。swicth.asmはCPUの動作モードをリアルモード(仮想86モード)とプロテクトモードの間で切り替えるためのものです。手順が多いですが、DPMIの生の姿に触れられておもしろいと思います。io.asmは32ビットプロテクトモード上から呼び出すための入出力ルーチンです。
実際にはDOSのファンクションコールを呼び出します。プログラムはDOS用のEXEファイルとして生成し、プログラムのエントリーポイントはswicth.asm内に設定します。switch.asm内でCPUモードを切り替え、main.asm内のルーチンへ飛びます。main.asmからリターンすると、swicth.asm内でプログラムを終了し、DOSへ戻ります。

; Copyright(C) 2004-2017 毛流麦花. All rights reserved.
; https://www.marbacka.net/blog/
; 転載禁止
; main.asm


.386
LOCALS
CR=0Dh
LF=0Ah


; -- 外部シンボルの参照 --
EXTRN WRITE:NEAR
EXTRN WRITENUM32:NEAR


; -- データセグメント(USE16)の定義 --
CommData SEGMENT PARA PUBLIC USE16 'DATA'
mes db '32ビットプロテクトモードからこんにちは', CR, LF, '$'
CommData ENDS


; -- コードセグメント(USE32)の定義 --
SEG32BIT SEGMENT DWORD PUBLIC USE32
 ASSUME CS:SEG32BIT,DS:CommData
 PUBLIC MainCode,SOF_MainCode     ; <-- switch.asmで参照するため
 MainCode PROC far
  SOF_MainCode label byte  ; <-- 32ビットプロテクトモードの入口
    mov  EDX, OFFSET mes
    call WRITE             ; 文字列を表示
    xor  EAX, EAX
    not  EAX               ; EAX=0FFFFFFFFh
    call WRITENUM32        ; 数値を表示
    db   66h
    retf                   ; <-- 32ビットプロテクトモードの出口
 MainCode ENDP
SEG32BIT ENDS


END

; Copyright(C) 2004-2017 毛流麦花. All rights reserved.
; https://www.marbacka.net/blog/
; 転載禁止
; switch.asm


.386p
CR=0Dh
LF=0Ah
STACKSIZE EQU 0500h
SIZE32BITSEG EQU 0FFFFh   ; コードセグメント(USE32)のセグメントリミットの値


; -- 外部シンボルの参照 --
EXTRN MainCode:far
EXTRN SOF_MainCode:byte
EXTRN EOF_MainCode:byte


; -- スタックセグメント(USE16)の定義 --
CommStack SEGMENT PARA STACK USE16 'STACK'
    db  STACKSIZE dup (?)
CommStack ENDS


; -- データセグメント(USE16)の定義 --
CommData  SEGMENT PARA PUBLIC USE16 'DATA'
 pmSwEnt      dd 0 ; リアル/仮想86 モード から (16Bit)プロテクトモードへ移行するための
                   ; エントリアドレス( セグメント[16]:オフセット[16] ) 
 selector     dw 0 ; 32ビットコードへの移行に使うセレクタ
 EntUse32     dd 0 ; (16Bit)プロテクトモードから32Bitプロテクトモードコードへ移行
                   ; するための エントリアドレス( セレクタ[16]:オフセット[16] )
 NODPMImsg    db '**ERROR**: このプログラムの実行には DPMIホストが必要です。',CR,LF,'$'
 NONSUP32BIT  db '**ERROR**: DPMIホストが 32bit API をサポートしてません.',CR,LF,'$'
 NODOSMEMmsg  db '**ERROR**: プライベートデータエリアを確保できません.',CR,LF
 NODOSMEMmsg2 db '( EXE HEADER の MAXALLOC を小さくしてください.)',CR,LF,'$'
 ERRpmSwmsg   db '**ERROR**: プロテクトモードへ移行するのに失敗しました.',CR,LF,'$'
 ERRgetDCPT   db '**ERROR**: LDTディスクリプタを割り当ててもらうのに失敗しました.',CR,LF,'$'
 ERRsetAdr    db '**ERROR**: セグメントベースアドレスの設定に失敗しました.',CR,LF,'$'
ERRsetAcsRigt db '**ERROR**: ディスクリプタのアクセス権の設定に失敗しました.',CR,LF,'$'
 ERRsetSegLim db '**ERROR**: セグメントリミットの設定に失敗しました.',CR,LF,'$'
 ERRfreeDCPT  db '**ERROR**: LDTディスクリプタの解放に失敗しました.',CR,LF,'$'
CommData  ENDS


; -- コードセグメント(USE16)の定義 --
BootCode    SEGMENT WORD PRIVATE USE16 'CODE'
         ASSUME CS:BootCode,DS:CommData,SS:CommStack
;------------------------------------------------------------
; [機能]
;    標準エラー出力にメッセージを出力する。
;    リアルモード・16ビットプロテクトモード・仮想86モード共用
; [引数]
;    DS:DX に文字列の先頭アドレスを入れる。(文字列の最後は '$' )
;    DS にセグメント(プロテクトモードの時はセレクタ)
;    DX にオフセット値を入れて neal call する。
; [返値]
;    なし
;------------------------------------------------------------
DISP_MESSAGE PROC near
    push AX
    push BX
    push CX
    push DX
    push SI
    pushf
    mov  BX, DX
    xor  SI, SI
 @@LOOP_COUNT:
    cmp  byte ptr [BX+SI],'$'
    je   short @@FIN_COUNT
    inc  SI
    jmp  @@LOOP_COUNT
 @@FIN_COUNT:
    mov  AH, 40h                ; Write Handle を使って書き込む
    mov  CX, SI
    mov  BX, 02h                ; 標準エラー出力
    int  21h
    popf
    pop  SI
    pop  DX
    pop  CX
    pop  BX
    pop  AX
    retn
DISP_MESSAGE ENDP
;
; EXEプログラムのエントリーポイント(リアル or 仮想86モード)
;
 program_start:
    mov  AX, CommData
    mov  DS, AX
    mov  AX, 1687h
    int  2Fh
    or   AX, AX              ; DPMI がインストールされてるか
    jz   short OKDPMI        ; --> インストールされてる
    mov  DX, OFFSET NODPMImsg; インストールされてない
    call DISP_MESSAGE
    mov  AX, 4C00h
    int  21h
 OKDPMI:                     ; インストールされてる
    and  BX, 0001h
    or   BX, BX              ; 32-Bit APIがサポートされてるか
    jnz  short OK32BITAPI    ; --> サポートされてる
    mov  DX, OFFSET NONSUP32BIT ;  サポートされてない
    call DISP_MESSAGE
    mov  AX, 4C00h
    int  21h
 OK32BITAPI:                 ; DPMIホストは32BIT APIをサポートしてる
                             ; この時点で CPU は 386 以上であることが確定
    mov  word ptr pmSwEnt, DI
    mov  word ptr pmSwEnt+2, ES
    or   SI, SI              ; プライベートデータエリアの確保は必要か
    jz   short NEXT1         ; --> 確保する必要なし
    mov  BX, SI              ; 確保する必要あり
    mov  AH, 48h
    int  21h
    jnc  short OKDOSMEM        ; --> 確保に成功
    mov  DX, OFFSET NODOSMEMmsg; 確保に失敗
    call DISP_MESSAGE
    mov  AX, 4C00h
    int  21h
 OKDOSMEM:                     ; 確保に成功
    mov  ES, AX
 NEXT1:
    mov  AX, 0001h             ; 32Bit 指定
    call pmSwEnt               ; セグメント間CALLでプロテクトモードに移行
    jnc short PM_OK            ; --> プロテクトモードへの移行に成功
    mov  DX, OFFSET ERRpmSwmsg ; プロテクトモードへの移行に失敗
    call DISP_MESSAGE
    mov  AX, 4C00h
    int  21h
 PM_OK:                        ; プロテクトモードへの移行に成功(16Bit コードセグメント)
    mov  AX, 0000h
    mov  CX, 0001h
    int  31h                   ; LDTディスクリプタを1個割り当ててもらう
    jnc  short NEXT2           ; --> LDTディスクリプタを割り当ててもらうのに成功
    mov  DX, OFFSET ERRgetDCPT ; LDTディスクリプタを割り当ててもらうのに失敗
    call DISP_MESSAGE
    mov  AX, 4C00h
    int  21h
 NEXT2:                        ; LDTディスクリプタを割り当ててもらうのに成功
    mov  selector, AX
    mov  BX, AX
    mov  DX, SEG MainCode
    xor  CX, CX
    shl  DX, 1
    rcl  CX, 1
    shl  DX, 1
    rcl  CX, 1
    shl  DX, 1
    rcl  CX, 1
    shl  DX, 1
    rcl  CX, 1
    ; 32ビットコードの先頭のオフセットアドレスを加える
    ASSUME DS:SEG MainCode
    mov  AX, OFFSET SOF_MainCode
    ASSUME DS:CommData
    ;
    add  DX, AX
    adc  CX, 00h               ; CX:DX <- 32Bitコードの入口のベースアドレス
    mov  AX, 0007h
    int  31h
    jnc  short NEXT3           ; --> セグメントベースアドレスの設定に成功
    mov  DX, OFFSET ERRsetAdr  ; セグメントベースアドレスの設定に失敗
    call DISP_MESSAGE
    mov  AX, 4C00h
    int  21h
 NEXT3:                        ; セグメントベースアドレスの設定に成功
    xor  EBX, EBX
    mov  BX, selector
    lar  EDX, EBX
    push EDX
    pop  AX
    pop  BX
    mov  CL, AH
    mov  CH, BL                ; CH:CL <- 32ビットコードのアクセス権
    bts  CX, 01h               ; W/R    R=1 (Read)
    btr  CX, 02h               ; E/C    C=0
    bts  CX, 03h               ; C/D    コードセグメント
    bts  CX, 07h               ; P      P=1 (Present on memory)
    bts  CX, 0Eh               ; B/D    デフォルト32Bit
    btr  CX, 0Fh               ; G      リミットはバイト単位
    mov  BX, selector
    mov  AX, 0009h
    int  31h
    jnc short NEXT4               ; --> ディスクリプタのアクセス権の設定に成功
    mov  DX, OFFSET ERRsetAcsRigt ; ディスクリプタのアクセス権の設定に失敗
    call DISP_MESSAGE
    mov  AX, 4C00h
    int  21h
 NEXT4:                           ; ディスクリプタのアクセス権の設定に成功
    mov  DX, SIZE32BITSEG         ; セグメントリミット
    xor  CX, CX
    mov  BX, selector
    mov  AX, 0008h
    int  31h
    jnc  short NEXT5              ; --> セグメントリミットの設定に成功
    mov  DX, OFFSET ERRsetSegLim  ; セグメントリミットの設定に失敗
    call DISP_MESSAGE
    mov  AX, 4C00h
    int  21h
 NEXT5:                           ; セグメントリミットの設定に成功
    mov  BX, selector
    xor  CX, CX
    mov  word ptr EntUse32, CX
    mov  word ptr EntUse32+2, BX
    call EntUse32                 ; FAR CALLで32ビットコードへ移行!!




    mov  BX, selector             ; 32ビットコードから、以下のコードで抜けてここへ戻る
    mov  AX, 0001h                ;     db 66h
    int  31h                      ;     retf
    jnc short NEXT6               ; --> LDTディスクリプタの解放に成功
    mov  DX, OFFSET ERRfreeDCPT   ; LDTディスクリプタの解放に失敗
    call DISP_MESSAGE
    mov  AX, 4C00h
    int  21h
 NEXT6:                           ; LDTディスクリプタの解放に成功
    mov  AX, 4C00h
    int  21h                      ; プログラムを終了してDOSに戻る
BootCode    ENDS


END program_start

; Copyright(C) 2004-2017 毛流麦花. All rights reserved.
; https://www.marbacka.net/blog/
; 転載禁止
; io.asm


.386
LOCALS


; -- データセグメント(USE16)の定義 --
CommData SEGMENT PARA PUBLIC USE16 'DATA'
workWRITENUM32     db 10 dup (?), '$'
CommData ENDS


; -- コードセグメント(USE32)の定義 --
SEG32BIT SEGMENT DWORD PUBLIC USE32
ASSUME CS:SEG32BIT,DS:CommData
;------------------------------------------------------------
; [機能]
;    文字列を標準出力へ出力
; [引数]
;    DS:(E)DX=文字列の先頭をさす セレクタ:オフセットの組
; [返値]
;    なし
; [注意]
;    文字列の最後は '$' にすること(これ自体は出力されない)
;------------------------------------------------------------
WRITE PROC near
PUBLIC WRITE
    push  EAX
    push  EBX
    push  ECX
    call  @@COUNT         ; 文字数を数える
    or    ECX, ECX
    jz    short @@FIN     ; 文字数が0なら何もしない
    mov   AH, 09h
    int   21h
 @@FIN:
    pop   ECX
    pop   EBX
    pop   EAX
    retn     ;  WRITEの出口

 @@COUNT:    ;  ECXに文字数をセット
    push  EDX
    movzx EDX, DX
    xor   ECX, ECX
  @@LOOP_COUNT:
    cmp   byte ptr [EDX+ECX], '$'
    je    short @@FIN_COUNT
    inc   ECX
    jmp   @@LOOP_COUNT
  @@FIN_COUNT:
    pop   EDX
    retn     ; @@COUNTの出口
WRITE ENDP
;------------------------------------------------------------
; [機能]
;    EAXの内容を10進数で標準出力へ出力
; [引数]
;    EAX=符号なし32bit数
; [返値]
;    なし
;------------------------------------------------------------
WRITENUM32 PROC near
PUBLIC WRITENUM32
    pushad
    mov  EDI, OFFSET workWRITENUM32

    mov  EBX, 3B9ACA00h     ; = 1,000,000,000
    call @@SUB
    mov  [EDI], CL

    mov  EBX, 05F5E100h     ; =   100,000,000
    call @@SUB
    mov  [EDI+1], CL

    mov  EBX, 00989680h     ; =    10,000,000
    call @@SUB
    mov  [EDI+2], CL

    mov  EBX, 000F4240h     ; =     1,000,000
    call @@SUB
    mov  [EDI+3], CL

    mov  EBX, 000186A0h     ; =       100,000
    call @@SUB
    mov  [EDI+4], CL

    mov  EBX, 10000d        ; =        10,000
    call @@SUB
    mov  [EDI+5], CL

    mov  EBX, 1000d         ; =         1,000
    call @@SUB
    mov  [EDI+6], CL

    mov  EBX, 100d          ; =           100
    call @@SUB
    mov  [EDI+7], CL

    mov  EBX, 10d           ; =            10
    call @@SUB
    mov  [EDI+8], CL

    add  EAX, 48d
    mov  [EDI+9], AL

    mov  ESI, EDI
    add  ESI, 09h
 @@LOOP:
    cmp  EDI, ESI
    jz   @@EXEC    ; 0 の時は '0' と CMP せずに表示
    cmp  byte ptr [EDI], '0'
    jnz  short @@EXEC
    inc  EDI
    jmp  @@LOOP
 @@EXEC:
    mov  EDX, EDI
    call WRITE
    popad
    retn           ; WRITENUM32の出口

@@SUB: ; EAXからEBXを何回引けるか, CL にアスキー文字で返す
    xor  CL, CL
 @@LOOP2:
    sub  EAX, EBX
    jc   short @@RET
    inc  CL
    jmp  @@LOOP2
 @@RET:
    add  EAX, EBX
    add  CL, 48d
    retn
WRITENUM32 ENDP
SEG32BIT ENDS


END

さっそく、アセンブル・リンクしてみましょう。なおリンクの順序に注意してください。EXEプログラムとしてのエントリーポイントはswitch.asmに設定します。以下の作業はWindowsXPのコマンドプロンプトで行っています。MS-DOSサブシステムの起動はWin9X(Windows95/98/Me)では不要です。(ただしWin9Xの場合、実行すると「プライベートデータエリアを確保できません」エラーになるかもしれません。)

Microsoft Windows XP [Version 5.1.2600]
(C) Copyright 1985-2001 Microsoft Corp.

D:\asm64>command     <--MS-DOSサブシステムの起動(実際には画面が切り替わる)

Microsoft (R) KKCFUNC バージョン 1.10
Copyright (C) Microsoft Corp. 1991,1993. All rights reserved.

KKCFUNC が組み込まれました.

マイクロソフトかな漢字変換  バージョン 2.51
(C)Copyright Microsoft Corp. 1992-1993

Microsoft(R) Windows DOS
             (C)Copyright Microsoft Corp 1990-2001.


D:\asm64>tasm main   <--main.asmのアセンブル
Turbo Assembler  Version 2.0  Copyright (c) 1988, 1990 Borland International

Assembling file:   main.ASM
Error messages:    None
Warning messages:  None
Passes:            1
Remaining memory:  337k


D:\asm64>tasm switch <--switch.asmのアセンブル
Turbo Assembler  Version 2.0  Copyright (c) 1988, 1990 Borland International

Assembling file:   switch.ASM
Error messages:    None
Warning messages:  None
Passes:            1
Remaining memory:  332k


D:\asm64>tasm io     <--io.asmのアセンブル
Turbo Assembler  Version 2.0  Copyright (c) 1988, 1990 Borland International

Assembling file:   io.ASM
Error messages:    None
Warning messages:  None
Passes:            1
Remaining memory:  335k


D:\asm64>tlink /3 switch+main+io  <--リンクしてswitch.exeを生成
Turbo Link  Version 3.0 Copyright (c) 1987, 1990 Borland International

D:\asm64>switch                   <--switch.exeを実行
32ビットプロテクトモードからこんにちは
4294967295
D:\asm64>


■解説

io.asmでは32ビットプロテクトモードからDOSのファンクションコールを呼び出しています。なぜこんなことができるのかと言うと、DPMIホストがトラップゲート経由でCPUをリアルモード(仮想86モード)に切り替え、対応するDOSのハンドラを呼び出してくれるからです。

執筆日:2004年9月26日(日)(www.marbacka.net内の別のサイトで公開)
最終更新日:2017年2月19日(日)

スポンサーリンク