1980年代のCP/Mから30年にわたり生き延びてきたデバッガ兼アセンブラが"debug.exe"です。
永らくDOSやWindowsに同梱されてきたdebug.exeですが、さすがにWindows7には含まれていないようです。
もし手元のPCがWindowsXP以前のOSなら、今の内に往時の16bitDOSアセンブラプログラミングを体験してみましょう。
なおdebugコマンドでのアセンブラの記述はIntel方式(destination, source)です。
参考資料:
コマンドプロンプトを開き、"debug" 又は "debug.exe" を実行します。
C:\>debug -
"-"がdebug用のコマンド入力プロンプトです。
既存のプログラムをデバッグすることも可能です。debug に続いてデバッグ対象のプログラム名を指定します。
C:\>debug.exe test.com -g =100 プログラムは正常に終了しました. -q
最初にパラメータの書式についてまとめておきます。
segment:offset形式又は単純なoffset形式。
1つ以上のバイト or 文字データ。
アドレス範囲を表現する形式。
以下、本記事で使用する主要なdebugコマンドを紹介していきます。その他のdebugコマンドについては冒頭で紹介した解説記事や"?"コマンドを参照して下さい。
コマンドやパラメータは、大文字小文字どちらで入力してもOKです。
-A ... 現在のIPからアセンブラ入力を開始 -A "Address" ... "Address"で指定されたアドレスからアセンブラ入力を開始
アセンブラ入力中は、現在入力中のアドレスがsegment:offset形式のプロンプトとして表示されます。
アセンブラ入力を終了してdebugコマンドプロンプトに戻るには、空行を入力します。
-G ... 現在のIPから実行開始 -G breakpoint ... 現在のIPから実行開始し、breakpoint("Address"形式)で停止 -G = start breakpoint ... start("Address"形式)から実行を開始、breakpointで停止 -G = start bp1 bp2 ... 上と同じだが、複数のbreakpointを設定可能
なおstartの値がoffset形式で指定された場合、"CS:offset"から開始される。
1つ以上の命令を実行後、レジスタ値を表示して停止。LOOPやサブルーチンの中は追わない。
-P ... 現在のIPから1命令実行して停止 -P number ... 現在のIPからnumber命令実行して停止 -P = start ... start("Address"形式)から1命令実行して停止 -P = start number ... start("Address"形式)からnumber命令実行して停止
1つ以上の命令を実行後、レジスタ値を表示して停止。LOOPやサブルーチンの中まで追うところが'P'と違う点。
-T ... 現在のIPから1命令実行して停止 -T number ... 現在のIPからnumber命令実行して停止 -T = start ... start("Address"形式)から1命令実行して停止 -T = start number ... start("Address"形式)からnumber命令実行して停止
メモリ範囲を指定して逆アセンブル表示。
-U ... 最後に'U'が実行されたアドレスから32byte逆アセンブル -U address ... 指定アドレスから32byte逆アセンブル -U range ... 指定範囲を逆アセンブル
以下のアセンブラをCS:0100から打ち込み、G/P/Tで実行してみましょう。
MOV CX,0005 MOV AX,0000 MOV BX,0000 ADD AX,CX INC BX LOOP 0109
> debug.exe -A 100 2CED:0100 mov cx,5 2CED:0103 mov ax,0 2CED:0106 mov bx,0 2CED:0109 add ax,cx 2CED:010B inc bx 2CED:010C loop 109 2CED:010E ※"2CED:xxxx"は勝手に表示されます。それ以降のアセンブラコードを 実際にキーボードから入力します。最後に空行でアセンブラ入力終了です。
ではIP=100から開始して、ADDコマンドの109hで止めてみます。
-G =100 109 AX=0000 BX=0000 CX=0005 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=2CED ES=2CED SS=2CED CS=2CED IP=0109 NV UP EI PL NZ NA PO NC 2CED:0109 01C8 ADD AX,CX
AX, BX, CXが"MOV"命令の通りになり、IPは109hを指しているのが分かります。
続いて"P"コマンドで1命令だけ実行してみます。
-P AX=0005 BX=0000 CX=0005 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=2CED ES=2CED SS=2CED CS=2CED IP=010B NV UP EI PL NZ NA PE NC 2CED:010B 43 INC BX
AXにCXの値が加算され、IPが次の命令を指します。
今度は"T"コマンドで、複数の命令を実行させてみます。"T"なのでLOOPの中も追ってくれます。
-T 5 AX=0005 BX=0001 CX=0005 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=2CED ES=2CED SS=2CED CS=2CED IP=010C NV UP EI PL NZ NA PO NC 2CED:010C E2FB LOOP 0109 AX=0005 BX=0001 CX=0004 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=2CED ES=2CED SS=2CED CS=2CED IP=0109 NV UP EI PL NZ NA PO NC 2CED:0109 01C8 ADD AX,CX AX=0009 BX=0001 CX=0004 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=2CED ES=2CED SS=2CED CS=2CED IP=010B NV UP EI PL NZ NA PE NC 2CED:010B 43 INC BX AX=0009 BX=0002 CX=0004 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=2CED ES=2CED SS=2CED CS=2CED IP=010C NV UP EI PL NZ NA PO NC 2CED:010C E2FB LOOP 0109 AX=0009 BX=0002 CX=0003 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=2CED ES=2CED SS=2CED CS=2CED IP=0109 NV UP EI PL NZ NA PO NC 2CED:0109 01C8 ADD AX,CX
LOOP → 109へ戻ってまたLOOP という動きを、1命令毎に細かく追うことが出来ます。
最後に"U"コマンドを試してみましょう。
-U 100 2CED:0100 B90500 MOV CX,0005 2CED:0103 B80000 MOV AX,0000 2CED:0106 BB0000 MOV BX,0000 2CED:0109 01C8 ADD AX,CX 2CED:010B 43 INC BX 2CED:010C E2FB LOOP 0109 2CED:010E 0000 ADD [BX+SI],AL 2CED:0110 0000 ADD [BX+SI],AL 2CED:0112 0000 ADD [BX+SI],AL 2CED:0114 0000 ADD [BX+SI],AL 2CED:0116 0000 ADD [BX+SI],AL 2CED:0118 0000 ADD [BX+SI],AL 2CED:011A 0000 ADD [BX+SI],AL 2CED:011C 3400 XOR AL,00 2CED:011E DC2C FSUBR QWORD PTR [SI]
開始アドレスしか指定していない為、32byte分逆アセンブルされました。
実際に入力したのは10Dまでなので、範囲を絞ってみましょう。
-U 100 10D 2CED:0100 B90500 MOV CX,0005 2CED:0103 B80000 MOV AX,0000 2CED:0106 BB0000 MOV BX,0000 2CED:0109 01C8 ADD AX,CX 2CED:010B 43 INC BX 2CED:010C E2FB LOOP 0109
範囲指定なので"L"を使うこともできます。
-U 100 L D 2CED:0100 B90500 MOV CX,0005 2CED:0103 B80000 MOV AX,0000 2CED:0106 BB0000 MOV BX,0000 2CED:0109 01C8 ADD AX,CX 2CED:010B 43 INC BX 2CED:010C E2FB LOOP 0109
アドレスにデータを書き込みます。
-E address ... 指定されたアドレスに1バイトデータを書き込みます。 address("Address"形式)がoffsetの場合はDS:offsetとして処理されます。 -E address list ... 指定されたアドレスから、listで指定されたシーケンシャルデータを 書き込みます。
なお、"list"には"String"形式も指定可能ですが、"\t""\r""\n"等のエスケープシーケンスは展開されません。またC言語のように末尾の0x00は付与されないので、必要なら明示的に指定する必要があります。
メモリ内容を16進ダンプします。
-D ... 最後にダンプしたアドレス or 初回ならDS:0からダンプ -D address ... 指定されたアドレスからダンプ -D range ... 指定されたメモリ範囲をダンプ
レジスタ内容を表示、あるいは新しい値を設定します。
-R ... 現在のレジスタ内容を表示 -R register ... 指定されたレジスタ内容を表示し、新しい値を入力
"register"には、'R'で表示されるレジスタ名(AX, BX, ...)の他にフラグレジスタの"F"も指定出来ます。
debugコマンド起動時点でのレジスタ値を確認してみます。
> debug.exe -R AX=0000 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=2CED ES=2CED SS=2CED CS=2CED IP=0100 NV UP EI PL NZ NA PO NC 2CED:0100 B90500 MOV CX,0005
DS, ES, SS, CS が全て同じセグメントになっています。IPが0100hに設定済です。CS:0 - CS:FFhはdebug.exe用の予約領域です。
SPはFFEEになっています。DS,CS,SS全て同じセグメントなので、あまりスタックにpushしすぎるとプログラムやデータ領域を壊してしまうかもしれませんので注意が必要です。
AXの値を変更してみます。
-r ax AX 0000 :
":"で新しい値の入力待ちです。"1234"(hex)を入力してみます。
-r ax AX 0000 :1234(リターン) -r AX=1234 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 ...
AXが更新されているのが分かります。
DS:0200hから"Hello World!"を書き込んでみましょう。まずは1byte単位で書き込んでみます。
-E 200 2CED:0200 00.
ここで入力待ちなので、そのまま16進で"48"(='H')を入力します。
-E 200 2CED:0200 00.48(リターン) -
引き続き、DS:0201に"e"(0x65)を書き込んでみます。
-E 201 2CED:0201 00.65(リターン)
DS:0202以降は"llo, World!"と文字列でまとめて書き込んでみます。
-E 202 "llo, World!" 00
では"D"コマンドでダンプしてみます。
-D 200 L F 2CED:0200 48 65 6C 6C 6F 2C 20 57-6F 72 6C 64 21 00 00 Hello, World!.. -
ちゃんと"Hello, World!"をメモリアドレス上に書き込めたことを確認出来ました。
ちなみに"\r\n\00"を末尾に指定するのであれば、
-E 200 "Hello, World!" 0D 0A 00
とする必要があります。
後述の'W'コマンドで書き出すファイル名を指定します。
-N [Drive:\path\]8_3_name.ext
ファイル名はDOSの8.3形式で指定する必要があります。
メモリイメージをファイルに出力します。出力するサイズをBXとCXレジスタで指定します。"BX:CX"と記述する場合もありますが、segment:offsetの計算ではなく、単純に高位16bitをBX、低位16bitをCXで指定します。したがって理論的には32bitサイズのメモリイメージをファイルに出力可能ですが、そもそも64KiBセグメントに囲まれた16bitDOSプログラミングの世界なので、BXは0で問題ないでしょう。
-W ... CS:0100h からBX:CXサイズ分出力 -W CS:0200 ... CS:0200h からBX:CXサイズ分出力
CS:0100hからのメモリイメージをファイルに書き込むと、結果としてCOMファイルが出力されます。DOS MZ実行形式ではありません。
ファイルサイズは最大 65280 Bytes で、これがそのままCS:0100h以降にロードされます。65280は丁度64KiB - FFhです。
CS:0000h - CS:00FFh はシステム(or debug.exe)の予約領域です。
N, W コマンドについては後述の"Hello, World!"プログラムで練習してみます。
ではいよいよdebugコマンドを使って "Hello, World!" を作ってみます。
使用するDOSファンクションは次の2つで充分でしょう。
AH=09h | DOS 1+ - WRITE STRING TO STANDARD OUTPUT |
AH=4Ch | DOS 2+ - EXIT - TERMINATE WITH RETURN CODE |
AH=09hで表示する文字列は、"$"終端になっている必要があります。
DOSファンクションコールの詳細は、"Ralf Brown's Interrupt List"を参照して下さい。
C:\>debug -A 100 2CED:0100 mov ah,9 2CED:0102 mov dx,200 2CED:0105 int 21 2CED:0107 mov ah,4c 2CED:0109 mov al,1 2CED:010B int 21 2CED:010D -E 200 "Hello, World!$" -N HELLOW.COM -R BX BX 0000 :0 -R CX CX 0000 :300 -W 00300 バイト書き込み中. -Q
実行してみます。
C:\>HELLOW.COM Hello, World! C:\>echo %ERRORLEVEL% 1
"Hello, World!"が表示され、戻り値(=1)もERRORLEVELを通じて確認出来ました。
debug.exeの主要コマンドと、実際に"Hello, World!"を組み立てる手順を見ていきました。
32bitから64bitへの移行が進んでいる2010年現在、16bitDOS環境のdebug.exeを使う機会は滅多にないと思いますが、もし万が一、使う場面に遭遇した時にこのページを思い出して頂ければ幸いです。