home ホーム search 検索 -  login ログイン  | reload edit datainfo version cmd icon diff delete  | help ヘルプ

Assembler/ForFun(x86_32)/02, 16bit DOS with NASM

Assembler/ForFun(x86_32)/02, 16bit DOS with NASM

Assembler / ForFun(x86_32) / 02, 16bit DOS with NASM
id: 783 所有者: msakamoto-sf    作成日: 2010-09-15 18:14:23
カテゴリ: Assembler 

Netwide Assembler(NASM)は80x86およびx86-64用のアセンブラで、MicrosoftやLinux, UNIX上の各種オブジェクトファイル・実行ファイルフォーマットに対応しています。Linux/UNIX/Windows各プラットフォームで動作し、アセンブラの入門レベルからOSの開発まで幅広く使われていています。

なおNASMアセンブラの記述はIntel方式(destination, source)です。

本記事ではNASMを使って16bitDOSプログラミング(但し.COMファイルフォーマットのみ)を体験してみます。
2010年9月時点での最新版 nasm-2.09.01 を使用し、 Windows XP SP3 (Pen4) 上で動作確認しています。

NASMのインストール方法の詳細については省略します。Web上で適宜検索・調査してインストールして下さい。



NASM版 "Hello, World!"

早速 Assembler/ForFun(x86_32)/01, 16bit DOS with debug.exe でdebugコマンドを使って作成した "Hello, World!" を作ってみましょう。
ファイル名は"hello_world01.asm"とします。開発環境やツール自体が16bitの制限から離れているので、ファイル名も8.3形式に囚われずに済みます。
hello_world01.asm:

org 100H
bits 16

section .text

start:
    MOV AH, 9H
    MOV DX, hello
    INT 21H
    MOV AH,4CH
    MOV AL,1H
    INT 21H

section .data
hello:
    db "Hello, World!$"

section .bss

ポイント:

  • "org 100h"で、実行時はDS:0100hから始まることを明示する。(ORGディレクティブ)
  • "bits 16"で、16bitモードを明示する。(BITSディレクティブ)

より厳密にCPUモードを指定したい場合は、"CPU"ディレクティブを使って命令セットを制限しても良いでしょう。

ex:
CPU 286  ; 80286の命令セットに制限する。

COMファイル生成におけるセグメントの扱いなど、詳細はNASMのドキュメントを参照して下さい。

コンパイル:

> nasm -fbin -o hello_world01.com hello_world01.asm

実行:

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

KKCFUNC が組み込まれました.

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

NASMには逆アセンブラも付属していますので、早速hello_world01.comを逆アセンブルしてみましょう。
"-b"でプロセッサモード、"-o"でオフセットを指定します。

> ndisasm -b 16 -o 100H hello_world01.com
00000100  B409              mov ah,0x9
00000102  BA1001            mov dx,0x110
00000105  CD21              int 0x21
00000107  B44C              mov ah,0x4c
00000109  B001              mov al,0x1
0000010B  CD21              int 0x21
0000010D  0000              add [bx+si],al
0000010F  004865            add [bx+si+0x65],cl
00000112  6C                insb
00000113  6C                insb
00000114  6F                outsw
00000115  2C20              sub al,0x20
00000117  57                push di
00000118  6F                outsw
00000119  726C              jc 0x187
0000011B  642124            and [fs:si],sp

"Hello, World!"のデータ部分まで逆アセンブルされてしまいましたが、実行コード部分はソースと同じコードに逆アセンブルされていることが確認出来ました。

"Hello, World!"改行+0終端バージョン

NASMの場合、バッククォートで囲むと改行などのエスケープシーケンスがASCIIコードに展開されます。
これを使って、改行付でHelloWorldを出力してみましょう。また、AH=09hの文字列出力だと"$"で終端させる必要がありましたが、1文字ずつ出力するAH=06hファンクションコールを使ってC言語と同様の0終端文字列に対応させてみます。
hello_world02.asm:

org 100H
bits 16

section .text

start:
    MOV AH, 6H
    MOV BX, hello
.print1:
    MOV DL, [DS:BX]
    CMP DL, 0H
    JZ .print1_end
    INT 21H
    INC BX
    JMP .print1
.print1_end:

    MOV AH,4CH
    MOV AL,1H
    INT 21H

section .data
hello:
    db `Hello, \r\nWorld!\r\n`, 0

section .bss

コンパイル+実行:

> nasm -fbin -o hello_world02.com hello_world02.asm
> hello_world02.com
Hello,
World!

文字列入力

文字入力を扱うDOSファンクションコールはいくつかのバリエーションが存在します。

AH = 01h DOS 1+ - READ CHARACTER FROM STANDARD INPUT, WITH ECHO
AH = 06h DOS 1+ - DIRECT CONSOLE INPUT
AH = 07h DOS 1+ - DIRECT CHARACTER INPUT, WITHOUT ECHO
AH = 08h DOS 1+ - CHARACTER INPUT WITHOUT ECHO
AH = 0Ah DOS 1+ - BUFFERED INPUT
AH = 0Ch DOS 1+ - FLUSH BUFFER AND READ STANDARD INPUT

今回は文字「列」としてバッファに保存してくれる AH=0Ah を使って名前を入力してもらい、「Hello, (入力された名前), welcome!」と表示してみます。

ソースの整理

「Hello, (入力された名前), welcome!」を表示するとなると、少なくとも「文字列の出力」を2回行うことになります。
ソースの機能としても、「文字列の出力」「文字列の入力」「プログラム終了」の3種類のDOSファンクションコールを呼ぶことになりますので、一旦それぞれのファンクションコールをサブルーチンとして切り出しておきましょう。

とりあえずhello_world02.asmを書き直して、C言語のstdcall呼び出し規約を意識してサブルーチン化してみました。
hello_world03.asm:

org 100H
bits 16

section .text

start:
    PUSH hello
    CALL print1
    PUSH 1H
    CALL exit

; arg1(1byte) : exit code
; return : non
exit:
    PUSH BP
    MOV BP, SP
    MOV AH, 4CH
    MOV AL, [BP+4]
    INT 21H

; arg1(2byte) : address of null-terminated string
; return : non
print1:
    PUSH BP
    MOV BP, SP
    MOV AH, 6H
    MOV BX, [BP+4]
.print1_loop:
    MOV DL, [DS:BX]
    CMP DL, 0H
    JZ .print1_end
    INT 21H
    INC BX
    JMP .print1_loop
.print1_end:
    POP BP
    RET 2

section .data
hello:
    db `Hello, \r\nWorld!\r\n`, 0

section .bss

メインルーチンがぐっと見やすくなりました。

ではこれに AH=0Ah ファンクションコールを導入してみましょう。

AH=0Ahのラッパー用サブルーチン

このファンクションコールは、DS:DXに専用の構造体アドレスを格納する必要があります。

offset size  description
00h    BYTE  読み込む文字数(改行含む)
01h    BYTE  戻り値で読み込まれた文字数(改行含まず)
02h    N*BYTE  読み込まれた文字データ+改行

サブルーチンのIFとしては、文字列を受け取るバッファのアドレスとバッファサイズの2つにしたほうが分かりやすいので、上述の構造体はサブルーチン内部でスタック上に構築してしまいましょう。

    PUSH バッファアドレス
    PUSH WORD バッファサイズ
    CALL gets

; arg1(2byte) : address of buffer
; arg2(2byte) : buffer length (including null terminator)
; return : non
gets:
    PUSH BP
    MOV BP, SP
    XOR AX, AX
    MOV AX, [BP+4]
    SUB SP, AX
    PUSH AX

構造体のoffset:01hについてですが、初回呼び出し時はとりあえず0でOKです。バッファサイズをPUSHするときにWORDサイズでPUSHしてもらうと、ちょうどAXで受け取りスタックに積み直すことで、リトルエンディアンなので offset 00h, 01h がバッファサイズ, 0の並びになります。
あとはINT21hを呼びます。

   XOR AX, AX
   MOV AH, 0AH
   MOV DX, SP
   INT 21H

続けて、末尾が改行になっていますので"0"に上書きします。

   ; retrieve "number of read chars"
   MOV BX, SP
   INC BX
   XOR AX, AX
   MOV AL, BYTE [BX]
   ; overwrite CR to 0
   MOV BX, SP
   ADD BX, 2
   ADD BX, AX
   MOV BYTE [BX], 0H

仕上げに、スタック上のバッファ内容を、引数で受け取ったバッファアドレスにコピーします。

   MOV SI, SP
   ADD SI, 2
   MOV DI, [BP+6]
   XOR CX, CX
   MOV CL, AL
   INC CX  ; for terminate character
   REP MOVSB

最後にスタックフレームを復元して戻ります。

   POP AX
   MOV AX, [BP+4]
   ADD SP, AX
   POP BP
   RET

完成!

「Hello, (入力された名前), welcome!」を表示する最終的なアセンブラコードは次のようになりました。
hello_world03.asm:

org 100H
bits 16

section .text

name_buf_len equ 10

start:
    PUSH BP
    MOV BP, SP

    PUSH prompt
    CALL print1

    PUSH name_buf
    PUSH WORD name_buf_len
    CALL gets

    PUSH hello_1
    CALL print1

    PUSH name_buf
    CALL print1

    PUSH hello_2
    CALL print1

    PUSH 1H
    CALL exit

; arg1(2byte) : address of buffer
; arg2(2byte) : buffer length (including null terminator)
; return : non
gets:
    PUSH BP
    MOV BP, SP
    XOR AX, AX
    MOV AX, [BP+4]
    SUB SP, AX
    PUSH AX

    XOR AX, AX
    MOV AH, 0AH
    MOV DX, SP
    INT 21H

    ; retrieve "number of read chars"
    MOV BX, SP
    INC BX
    XOR AX, AX
    MOV AL, BYTE [BX]
    ; overwrite CR to 0
    MOV BX, SP
    ADD BX, 2
    ADD BX, AX
    MOV BYTE [BX], 0H

    MOV SI, SP
    ADD SI, 2
    MOV DI, [BP+6]
    XOR CX, CX
    MOV CL, AL
    INC CX  ; for terminate character
    REP MOVSB

    POP AX
    MOV AX, [BP+4]
    ADD SP, AX
    POP BP
    RET

; arg1(1byte) : exit code
; return : non
exit:
    PUSH BP
    MOV BP, SP
    MOV AH, 4CH
    MOV AL, [BP+4]
    INT 21H

; arg1(2byte) : address of null-terminated string
; return : non
print1:
    PUSH BP
    MOV BP, SP
    MOV AH, 6H
    MOV BX, [BP+4]
.print1_loop:
    MOV DL, [DS:BX]
    CMP DL, 0H
    JZ .print1_end
    INT 21H
    INC BX
    JMP .print1_loop
.print1_end:
    POP BP
    RET 2

section .data
prompt:
    db "What's your name ? : ", 0
hello_1:
    db `\r\nHello, `, 0
hello_2:
    db `, welcome!\r\n`, 0

section .bss
name_buf resb name_buf_len

コンパイル+実行:

> nasm -fbin -o hello_world03.com hello_world03.asm
> hello_world03.com
What's your name ? : 123456789
Hello, 123456789, welcome!

>hello_world03.com
What's your name ? : FooBar
Hello, FooBar, welcome!

>

コマンドラインと環境変数を取得してみる

Assembler/ForFun(x86_32)/01, 16bit DOS with debug.exe の記事では取りあげませんでしたが、コマンドラインや環境変数を取得することも出来ます。
MS-DOSの場合、COM or EXEがロード後、"Program Segment Prefix"(PSP)という領域を参照するとコマンドラインや環境変数を取り出せるようになっています。

"Program Segment Prefix" 参考資料:

環境変数を表示してみる

PSPの0x2C, 2Dで示されたセグメントの先頭から環境変数を取得出来ます。一つのエントリは'0'終端で、最後のエントリの後には'0'が二つ続きます。
ざっくりロジックを作ってみると、こうなりました。ロード直後は、PSPのセグメントはCOM/EXEともにDSに保存されています。

start:
    PUSH BP
    MOV BP, SP

    MOV BX, [DS:002Ch]
    MOV DS, BX

    MOV BX, 0000H

.loop2:
    PUSH BX
    CALL print1
.loop1:
    ; 1エントリの'0'終端までBXポインタを進めます。
    INC BX
    CMP BYTE [DS:BX], 0
    JNZ .loop1
    ; 1文字前進させて'0'なら、'0'が2つ続いたので全エントリを表示し終わったことを意味します。
    INC BX
    CMP BYTE [DS:BX], 0
    JNZ .loop2

    PUSH 1H
    CALL exit

しかしこれだと改行が無いので見づらいです。そこで".data"セクションにCRLFだけの文字列をおいて、1エントリ分表示した後にCRLFを表示させてみます。
ここで注意が必要です。エントリをチェックするメインのループでは、DSは環境変数のセグメントを指しています。しかしCRLFの文字列は実行コードと同じCSセグメントに存在します。
そのため、CRLFを表示するのであれば一時的にセグメントを切替え、表示を終えたらまた環境変数のセグメントに戻す必要があります。なお起動直後はCSとDSは同じ値になっていますので、ここではDSを保存し、切り替えに使います。

  1. プログラム起動時点でのDSを保存。
  2. CRLF出力の箇所だけ、保存しておいたDSに切り替える。

この2点を盛り込み、改行コード付で表示出来るようにしたのが次のソースコードです。
env.asm:

org 100H
bits 16

section .text

start:
    PUSH BP
    MOV BP, SP

    PUSH DS            ; スタック上にプログラム起動時点でのDSを保存
    MOV BX, [DS:002Ch]
    MOV [segenv], BX   ; 環境変数のセグメントを保存
    MOV DS, BX         ; 環境変数のセグメントに切替

    MOV BX, 0000H

.loop2:
    PUSH BX
    CALL print1

    POP DS            ; プログラム起動時点のDSを復元
    PUSH crlf
    CALL print1
    PUSH DS
    MOV DS, [segenv]  ; 環境変数のセグメントに切替

.loop1:
    INC BX
    CMP BYTE [DS:BX], 0
    JNZ .loop1
    INC BX
    CMP BYTE [DS:BX], 0
    JNZ .loop2

    POP DS
    PUSH 1H
    CALL exit

; arg1(1byte) : exit code
; return : non
exit:
    PUSH BP
    MOV BP, SP
    MOV AH, 4CH
    MOV AL, [BP+4]
    INT 21H

; arg1(2byte) : address of null-terminated string
; return : non
print1:
    PUSH BP
    MOV BP, SP
    PUSH BX
    PUSH DX
    MOV AH, 6H
    MOV BX, [BP+4]
.print1_loop:
    MOV DL, [DS:BX]
    CMP DL, 0H
    JZ .print1_end
    INT 21H
    INC BX
    JMP .print1_loop
.print1_end:
    POP DX
    POP BX
    POP BP
    RET 2

section .data
crlf:
    db `\r\n`, 0

section .bss
segenv:
    resw 1

コンパイル:

> nasm -fbin -o env.com env.asm

実行結果は自明なので省略します。

コマンドラインを表示してみる(1)

続いてコマンドラインを表示してみます。
PSPのドキュメントを調べていくと、80hにコマンドラインの長さが、81hからコマンドラインに渡された文字列が格納されていることが分かります。
終端文字列は不定のようなので、80hで取得された文字数分だけ、1文字ずつ表示してみます。

cmdline1.asm:

org 100H
bits 16

section .text

start:
    PUSH BP
    MOV BP, SP

    XOR CX, CX
    MOV CL, [DS:0080h]
    CMP CX, 0
    JZ .loop_end
    MOV AH, 02H
    XOR DX, DX
    MOV BX, 81H
.loop:
    MOV DL, [BX]
    INT 21H
    INC BX
    LOOP .loop
.loop_end:

    PUSH 1H
    CALL exit

; arg1(1byte) : exit code
; return : non
exit:
    PUSH BP
    MOV BP, SP
    MOV AH, 4CH
    MOV AL, [BP+4]
    INT 21H

section .data

section .bss

コンパイル+実行:

> nasm -fbin -o cmdline1.com cmdline1.asm
> cmdline1.com

> cmdline1.com a b c d
 a b c d
>

コマンドラインを表示してみる(2)

コマンドライン文字列を表示出来ましたが、自分自身のプログラム名が表示されていません。
MicrosoftのKB自分自身のプログラム名については、KB123729を見ると環境変数のブロックに続けて格納されているようです。
実際にdebugコマンドで見てみると、環境変数のブロック終端である二つの'0'の後ろに、"01 00"が続き、その後にプログラム名が格納されています。

> debug cmdline1.com
-D 2C 2D
2D57:0020                                      FE 2C                     .,
-D 2CFE:0500 L 100
...
2CFE:0560  33 30 20 54 33 00 00 01-00 43 4D 44 4C 49 4E 45   30 T3....CMDLINE
                                ^^^^^
2CFE:0570  31 2E 43 4F 4D 00 00 00-48 57 00 00 00 00 00 00   1.COM...HW......

終端文字列はとりあえず'0'と仮定して良さそうです。

環境変数を表示した時のenv.asmを改造した cmdline2.asm は以下のようになりました。

org 100H
bits 16

section .text

start:
    PUSH BP
    MOV BP, SP

    PUSH DS
    MOV BX, [DS:002Ch]
    MOV [segenv], BX
    MOV DS, BX

    MOV BX, 0000H

.loop1:
    INC BX
    CMP BYTE [DS:BX], 0
    JNZ .loop1
    INC BX
    CMP BYTE [DS:BX], 0
    JNZ .loop1

    ADD BX, 3       ; "01 00"をスキップ
    PUSH BX
    CALL print1

    POP DS
    PUSH 1H
    CALL exit

; arg1(1byte) : exit code
; return : non
exit:
(以下 env.asm と同じ)

コンパイル+実行:

> nasm -fbin -o cmdline2.com cmdline2.asm
> cmdline2.com
C:\(...)\CMDLINE2.COM
>

上手く動いてくれたようです。
なおdebug経由で起動した時は"CMDLINE2.COM"しか格納されていませんが、単体で起動するとフルパスで格納されるようです。

まとめ

NASMを使って16bitDOSプログラム(COMフォーマット)を組み立てる手順を見ていきました。
またDOS環境における環境変数やコマンドライン文字列の取得方法についても見ていきました。
32bitから64bitへの移行が進んでいる2010年現在、16bitDOSプログラムをアセンブラで開発する機会は滅多にないと思いますが、もし万が一、そのような機会に遭遇した時にこのページを思い出して頂ければ幸いです。



プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2010-09-19 21:02:53
md5:86220016989570f11125cca2e1987f53
sha1:857cd24b021b5f647c3a9b3755081132e3c08076
コメント
コメントを投稿するにはログインして下さい。