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