16bit programming in Windows 95

前回 MS-DOS で QuickAssembler を動かしてみたのはいいものの、32bit のアセンブリ言語をコンパイルできないことに気づいてしまったので、結局 Windows を入れることに。

しかし Hyper-V で Windows 95 のインストーラーを実行すると、仮想マシンがハングしてしまう現象に遭遇。Windows 95 のインストーラーはマウスに対応しているため、このハングの原因はおそらく MS-DOS の MOUSE コマンドを実行するとハングしてしまう現象と同じ気がします。Hyper-V 使えないじゃん、ということで ESXi 5.1 で試してみたら、こちらは問題なし。やっぱり開発用途には VMware に軍配が上がります。

OS のインストール後、開発環境として最初は何も考えずに Visual C++ 6.0 を入れてみたのですが、Visual C++ 6.0 は 16bit アプリケーションの開発に対応していなかった、ということに初めて気づき、以下の 2 つを入れ直しました。何とも世話の焼ける・・

  • VIsual C++ 1.52
  • MASM 6.11

セットアップ中の画面の一部を紹介します。まずは Windows 95。実に懐かしい。

image image

image

十数年ぶりに触ってみると、今では当然のように使っている機能がまだ実装されていなくて驚きます。例えば・・

  • コマンド プロンプトが cmd.exe じゃなくて command.com
  • コマンド プロンプトで矢印キー、 Tab キーが使えない
  • コマンド プロンプトで、右クリックを使ってクリップボードからの貼り付けができない
  • メモ帳で Ctrl+A、Ctrl+S が使えない
  • エクスプローラーにアドレスバーがない

以下は Visual C++ と MASM のインストール画面を抜粋したもの。

image image

MASM 6.11 のインストーラーは CUI。

image

DOS, Windows, WIndows NT の全部に対応しているらしい。

image

MASM はいろいろと注意事項が多い。

image image

image

インストールが完了したら、環境変数の設定を行なうためのバッチ ファイルを作ります。サンプルのスクリプトがインストールされているので、それを組み合わせるだけです。

@echo off
set PATH=C:\MSVC\BIN;C:\MASM611\BIN;C:\MASM611\BINR;%PATH%
set INCLUDE=C:\MSVC\INCLUDE;C:\MSVC\MFC\INCLUDE;C:\MASM611\INCLUDE;%INCLUDE%
set LIB=C:\MSVC\LIB;C:\MSVC\MFC\LIB;C:\MASM611\LIB;%LIB%
set INIT=C:\MSVC;C:\MASM611\INIT;%INIT%

MASM、VC++ ともに NMAKE を持っていますが、VC++ 1.52 に入っている NMAKE のバージョンが新しかったので、VC++ を先頭にしました。

環境ができたところで、今回は 「はじめて読む 486」 の例題を試してみます。本文に掲載されている例題は、Borland C++ 3.1/Turbo Assembler 3.2 用の文法になっているため、細かいところは VC++/MASM 用に書き換えないといけません。というわけでこんな感じ。

まずは C ソースファイル main.c。

#include <stdlib.h>
#include <stdio.h>

extern short GetVer();
extern void RealToProto();
extern void ProtoToReal();

void main(int argc, char **argv) {
    printf("Hello, MS-DOS%d!\n", GetVer());
    RealToProto();
    ProtoToReal();
    printf("Successfully returned from Protected mode.\n");
    exit(0);
}

次がアセンブリ utils.asm。

.386
.MODEL small
.code

;* GetVer – Gets DOS version.
;*
;* Shows:   DOS Function – 30h (Get MS-DOS Version Number)
;*
;* Params:  None
;*
;* Return:  Short integer of form (M*100)+m, where M is major
;*          version number and m is minor version, or integer
;*          is 0 if DOS version earlier than 2.0

_GetVer  PROC

        mov     ah, 30h                 ; DOS Function 30h
        int     21h                     ; Get MS-DOS version number
        .IF     al == 0                 ; If version, version 1
        sub     ax, ax                  ; Set AX to 0
        .ELSE                           ; Version 2.0 or higher
        sub     ch, ch                  ; Zero CH and move minor
        mov     cl, ah                  ;   version number into CX
        mov     bl, 100
        mul     bl                      ; Multiply major by 10
        add     ax, cx                  ; Add minor to major*10
        .ENDIF
        ret                             ; Return result in AX

_GetVer  ENDP

public _RealToProto
_RealToProto    proc    near
                push bp
                mov bp, sp
                ;
                mov eax, cr0
                or eax, 1
                mov cr0, eax
                ;
                jmp flush_q1
flush_q1:
                pop bp
                ret
_RealToProto    endp

public _ProtoToReal
_ProtoToReal    proc    near
                push bp
                mov bp, sp
                ;
                mov eax, cr0
                and eax, 0fffffffeh
                mov cr0, eax
                ;
                jmp flush_q2
flush_q2:
                pop bp
                ret
_ProtoToReal    endp

        END

最後に Makefile。QuickC のときよりは現在の書式に近づきましたが、相変わらずリンカへの入力の渡し方がおかしい。

PROJ = TEST
USEMFC = 0
CC = cl
ML = ml
CFLAGS =/nologo /W3 /O /G3
LFLAGS =/NOLOGO /ONERROR:NOEXE
AFLAGS =
LIBS =
MAPFILE =nul
DEFFILE =nul

all: $(PROJ).EXE

clean:
    @del *.obj
    @del *.exe
    @del *.bnd
    @del *.pdb

UTILS.OBJ: UTILS.ASM
    $(ML) $(AFLAGS) /c UTILS.ASM $@

MAIN.OBJ: MAIN.C
    $(CC) $(CFLAGS) /c MAIN.C $@

$(PROJ).EXE:: MAIN.OBJ UTILS.OBJ
    echo >NUL @<<$(PROJ).CRF
MAIN.OBJ +
UTILS.OBJ
$(PROJ).EXE
$(MAPFILE)
$(LIBS)
$(DEFFILE)
;
<<
    link $(LFLAGS) @$(PROJ).CRF
    @copy $(PROJ).CRF $(PROJ).BND

プログラムはとても単純で、前回と同様に int 21 で DOS のバージョンを表示してから、コントロール レジスタ 0 の PE ビットを変更して特権レベルを変更し、また戻ってくる、というものです。ただし上記のコードでは、C、アセンブリのコンパイルは通るものの、以下のリンクエラーが出てうまくいきません。

UTILS.OBJ(UTILS.ASM) : fatal error L1123: _TEXT : segment defined both 16- and 32-bit

image

ここで 2 時間ぐらいハマりました。リンクエラーの原因は単純で、VC++ 1.52 のコンパイラは 16bit コードを生成しているのに対し、MASM は 32bit コードを生成しているためです。エラー メッセージの通り、16bit と 32bit の 2 つのコード セグメントを含む実行可能ファイルを生成できないためのエラーです。

このプログラムで確かめたいのは、16bit リアル モードから PE ビットを変更してプロテクト モードに移行し、再び 16bit リアル モードに戻ってくる動作です。したがって生成されるべき EXE は 16bit アプリケーションであり、VC++ の生成する main.obj は問題なく、MASM が 16bit コードを生成するように指定したいところです。

MASM が 32bit コードを生成する理由は、utils.asm の先頭の .386 で CPU のアーキテクチャを指定しているためです。これがないと、32bit レジスタの eax などが使えません。CPU のアーキテクチャを指定したことで、その後の .code セグメント指定が自動的に 32bit コード セグメントになってしまっています。

32bit 命令を有効にしつつ、セグメントは 16bit で指定する方法が見つかればよいわけです。NASM や GNU Assembler だと簡単に指定できるようですが・・いろいろと探して、以下のフォーラムを見つけました。10 年前に同じ悩みを抱えている人がいた!

Link Fatal Error 1123
http://www.masmforum.com/board/index.php?PHPSESSID=786dd40408172108b65a5a36b09c88c0&topic=1382.0

ファイルの先頭で .MODEL と CPU 指定を入れ替えればいいらしい。こんなん知らんて。

.MODEL small
.386

.code

;* GetVer – Gets DOS version.
;*
;* Shows:   DOS Function – 30h (Get MS-DOS Version Number)
;*
;* Params:  None
;*
;* Return:  Short integer of form (M*100)+m, where M is major
;*          version number and m is minor version, or integer
;*          is 0 if DOS version earlier than 2.0

_GetVer  PROC

以下、ずっと同じなので省略

これで無事ビルドが成功し、実行できました。文字が出力されているだけなので、本当にプロテクト モードに変わったかどうか疑わしくはありますが、まあ大丈夫でしょう。16bit の道は険しい。

image

広告

MS-DOS 6.22 on Hyper-V

CPU の仕組みについて書かれた本の中でも、「はじめて読む 486」 はかなり有名な名著と言えるはずです。

はじめて読む486―32ビットコンピュータをやさしく語る
http://www.amazon.co.jp/%E3%81%AF%E3%81%98%E3%82%81%E3%81%A6%E8%AA%AD%E3%82%80486%E2%80%9532%E3%83%93%E3%83%83%E3%83%88%E3%82%B3%E3%83%B3%E3%83%94%E3%83%A5%E3%83%BC%E3%82%BF%E3%82%92%E3%82%84%E3%81%95%E3%81%97%E3%81%8F%E8%AA%9E%E3%82%8B-%E8%92%B2%E5%9C%B0-%E8%BC%9D%E5%B0%9A/dp/4756102131

途中で挫折していたので、最初からちゃんと読んでいたところ、やっぱり実機でサンプル コードを試したくなります。ちょっと前にブート コードの勉強をしていたときは、仮想ディスク (VHD) のマスター ブート レコード (MBR) をエディタで書き換えて、 Hyper-V の仮想マシンをその VHD から起動して動かすというけっこう面倒な方法を取っていました。

もっと楽に 16 ビット リアルモードで遊べる環境を作っておこう、ということで、MS-DOS を Hyper-V 仮想マシンにインストールしてみました。何も MS-DOS まで遡らなくても、Windows 9x や ME の DOS モードを使えばいいのですが、まあそこは好奇心ということで。どうでもいい昔話をすると、初めてプログラミングというものに触れたのが中一のときに学校にあった PC-9801 の N88-BASIC で、その後すぐに Windows 95 (年から考えると 98 だったかも) が導入されて Visual Basic/Visual C++ で遊び始めたので、Windows 以前の MS-DOS は全然触ったことがありません。そういえば当時の起動フロッピー ディスクはまだ手元にあるかも・・。

閑話休題、MS-DOS ですが、インストーラーは MSDN サブスクリプションに MS-DOS 6.0 と 6.22 が含まれていたのでそれを使います。MSDN を持っていない人は、"MS-DOS 6.22 download" とかのキーワードでググると、見つかるはずなので自己責任でどうぞ。

手順は以下の通り。6.22 はアップグレード用で、新規インストールはできないので、6.0 を入れてから 6.22 にアップグレードします。

  1. 適当な Windows マシンで MS-DOS 起動ディスクを作る
  2. MSDN からインストールしたファイルを仮想フロッピー、もしくは VHD にコピー
  3. 起動ディスクから DOS を起動
  4. MS-DOS 6.0 をフロッピーにインストール
  5. MS-DOS 6.0 を起動
  6. MS-DOS 6.22 を VHD にインストール

MSDN でダウンロードできるインストーラー en_msdos60.exe や en_msdos622.exe は自己解凍書庫で、解凍すると MS-DOS アプリの setup.exe が出てきます。これを使うためにはそもそも DOS を起動しないといけません。ということで、まずは DOS の起動ディスクを作らないといけません。

まずは空の仮想フロッピー ファイル (.vfd) を作ります。普通は PowerShell の New-Vfd コマンドレットを使うところですが、このコマンドレットは規定サイズの空ファイルを作っているだけなので、fsutil でも代用できます。サイズの 1474560 = 1440 * 1024 は、2HD フロッピーのバイト数です。

D:\MSWORK\dos> fsutil file createnew floppy.vfd 1474560
File D:\MSWORK\dos\floppy.vfd is created

これを仮想マシンにマウントし、エクスプローラーからフォーマットの画面を起動すると、なんと Windows 8.1 でも "Create an MS-DOS startup disk" のチェックボックスが使えます。よく廃止されないものです。ちなみに FORMAT コマンドにはそれに該当するようなオプションは見当たりません。GUI でしかできないんでしょうかね。

01

次に仮想マシンを作ります。RAM は 32MB、HDD は 1GB にしておきます。仮想 BIOS の設定で、Floppy を一番上に移動させます。こんな感じです。

image

先ほど作った起動ディスクをマウントして起動すると、無事 DOS が起動します。バージョンは Windows Millenium [Version 4.90.3000] だそうです。すぐには使わないので、仮想マシンはシャットダウンします。shutdown コマンドなんておものは存在せず、いきなり Turn off で問題ありません。

image

さて次に、DOS インストーラーの準備ですが、MS-DOS 6.0、6.22 共にインストール ファイルの合計サイズが 1.44MB を超えるので、VFD ではなく VHD で作ります。MS-DOS 6.22 の方は、親切にも複数のディスク イメージ ファイル (.img) をそれぞれフロッピーに書き込むためのスクリプトが用意されていますが、ディスクの入れ替えが面倒なだけなので使いません。

New-VHD コマンドレットなどで VHD を作って、MBR ディスクとして初期化し、FAT でフォーマットします。NTFS だと読めないので注意です。インストーラーのファイルは、おそらくドライブのルート ディレクトリ直下に入れておかないと動かないはずなので、それぞれのインストール用 VHD を作らないといけません。

作った VHD を仮想マシンにマウントし、再度起動ディスクから DOS を起動します。構成はこんな感じ。

image

1GB の VHDX はまだフォーマットされていないので認識されず、C: がインストーラーのドライブになります。したがって、C:\SETUP と実行します。

image

なぜか認識できないパーティションがあるとか言われます。よく分からないのでそのまま続行します。

image

MS-DOS 6.0 のインストーラーにはハード ディスクをフォーマットする機能が含まれず、空のハードディスクが見つからなかったため、フロッピーにインストールすることになります。仮にフォーマット済みの VHD をマウントして VHD にインストールしようとしても、有効な DOS が見つかりませんでした、というエラーが出てインストールできないので、最初の MS-DOS はフロッピーにインストールする必要があります。とりあえずこの画面ではそのまま Enter キーを押します。

image

この画面も Enter で続行します。

image

フロッピーを入れろと言われるので、ここで新たに VFD ファイルを作ってマウントしてから Enter キーを押します。

image

フロッピーの種類を聞かれるので、1.44MB を選びます。

image

無事終わりました。Enter キーを押してインストーラーを終了します。

image

元の DOS 画面でエラーが出ますが、おそらく起動ディスクを抜いてしまったためなので、気にせず仮想マシンを再起動します。

image

無事、MS-DOS 6.00 が起動しました。

image

次に MS-DOS 6.22 のインストールに移ります。手順はほとんど同じです。今度はハードディスクにインストールしたいので、インストール先の VHD を予め FAT でフォーマットしておきます。

今度は C: が空ディスク、D: がインストーラーなので D:\SETUP と実行します。

image

また未フォーマットのパーティションが検出されます。そんなのどこにあるんだ・・・気にせず続行します。

image

今度は最小構成ではなく、通常の構成でハードディスクにインストールしてくれそうです。ここも Enter を押します。

image

アンインストール ディスクというものを作る必要があるようです。今でいうリカバリ ディスクでしょうか。ここではそのまま Enter を押します。いちいちラベルを貼る猶予を与えてくれる親切設計。

image

インストール設定の確認です。C:\DOS にインストールされるようです。これぞ C:\WINDOWS の前身!

image

オプションのソフトウェアについて聞かれます。この当時のアンチ ウィルスって一体。Undelete ってのも謎。ごみ箱的な機能・・?とりあえず全部インストールしておきます。

image

いよいよ開始です。

image

フロッピーを入れ替える時間です。

image

サイズは 1.44MB です。

image

まさかのインストール エラー。ISK って何のことか分からず、わりと為す術がない。最初からのステップを 2 回繰り返しましたが、このエラーは変わらず。同じファイルを別の Windows 8.1 上の Hyper-V で試したら特にエラーは出ないので、Windows Server 2012 の Hyper-V との相性が悪いとかだったりして。詳細は不明です。

image

一応インストールは終わりました。フロッピーを抜いて Enter を押します。

image

Enter を押して再起動します。

image

起動には問題ないようです。HIMEM ってやつが結構遅い。

image

次に、どこからともなく入手してきた QuickC with Quick Assembler 2.51 をインストールします。ディスクが 10 枚もあって面倒くさい・・。

image

インストール後、何やら重要そうなことが書かれている画面が出てくるのでキャプチャーしておく。

image

インストールが終わったら、先ほどキャプチャーした画面の記述にしたがって環境変数の設定。現在の C:\AUTOEXEC.BAT はこんな感じ。

@ECHO OFF
PROMPT $p$g
PATH C:\DOS
SET TEMP=C:\DOS

これを、C:\QC25\BIN\NEW-VARS.BAT を参考にして、以下のように変更。MOUSE コマンドを実行してマウス ドライバーを有効にすると、高確率で OS がハングするので、マウスは諦めます。

@ECHO OFF
PROMPT $p$g
SET PATH=C:\QC25\BIN;C:\QC25\TUTORIAL;C:\DOS

SET LIB=C:\QC25\LIB
SET INCLUDE=C:\QC25\INCLUDE

SET TEMP=C:\DOS

あと config.sys。懐かしすぎる。C:\QC25\BIN\NEW-CONF.SYS には FILES=20, BUFFERS=10 と書かれていましたが、FILES は既に 30 になっていたので、BUFFERS を多めの 20 にしておくことに。

DEVICE=C:\DOS\SETVER.EXE
DEVICE=C:\DOS\HIMEM.SYS
DOS=HIGH
FILES=30
SHELL=C:\DOS\COMMAND.COM C:\DOS\  /p
BUFFERS=20

再起動して、早速 QuickC を起動。

image

これが 20 年前の IDE 。。というか画面ちっさ。Hyper-V のせいだけど。

image

ただ普通に Hello World を書いても面白くないので、以下のような 3 つのファイルを書いて NMAKE でビルド。

まずは C のソース。これは普通です。

//
// 00.C
//

#include <stdio.h>

extern int GetVer();

void main(int argc, char **argv)
{
    printf("Hello, MS-DOS %d!", GetVer());
    exit(0);
}

次にアセンブリ言語。サンプルからコピペしただけです。int 21 でバージョンが返ってくるみたいです。

;
; UTILS.ASM
;

    .MODEL    small, c
    .CODE

;* GetVer – Gets DOS version.
;*
;* Shows:   DOS Function – 30h (Get MS-DOS Version Number)
;*
;* Params:  None
;*
;* Return:  Short integer of form (M*100)+m, where M is major
;*        version number and m is minor version, or 0 if
;*        DOS version earlier than 2.0

GetVer    PROC

    mov    ah, 30h       ; DOS Function 30h
    int    21h           ; Get MS-DOS Version Number
    cmp    al, 0         ; DOS version 2.0 or later?
    jne    @F            ; Yes?    Continue
    sub    ax, ax        ; No?  Set AX = 0
    jmp    SHORT exit    ;   and exit
@@:    sub    ch, ch     ; Zero CH and move minor
    mov    cl, ah        ;   version number into CX
    mov    bl, 100
    mul    bl            ; Multiply major by 10
    add    ax, cx        ; Add minor to major*10
exit:    ret             ; Return result in AX

GetVer    ENDP

    END

最後が MAKEFILE。サンプルのファイルを元にいろいろと修正しましたが、リンカの設定のところが意味不明・・。

PROJ    =TEST
CC      =qcl
AS      =qcl
CFLAGS=/Od /Gi$(PROJ).mdt /DNDEBUG /Zi /Zr
AFLAGS=/Zi /Cx /P1
LFLAGS  =/NOI /INCR /CO

.asm.obj: ; $(AS) $(AFLAGS) -c $*.asm

all:    $(PROJ).EXE

utils.obj:        utils.asm

00.obj: 00.c

$(PROJ).EXE:    utils.obj 00.obj
    echo >NUL @<<$(PROJ).crf
utils.obj +
00.obj
$(PROJ).EXE
NUL.MAP

<<
    ilink -a -e "qlink $(LFLAGS) @$(PROJ).crf" $(PROJ)

ファイルを作ったら NMAKE するだけです。

image

おお、MS-DOS のバージョンが出た!これでリアルモードと仲良くできそう。

image

しかしファイルの編集がやりにくすぎる・・・SSH みたいなリモート接続はできないのだろうか。そもそも Hyper-V の仮想 NIC に対応している TCP/IP のドライバーなんてなさそう。