前回 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。実に懐かしい。

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

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

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

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

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

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

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

@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

ここで 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 の道は険しい。