「いまどき使う?」の感もありますが、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日(日)