5.1 はじめに

HSPのスクリプトでも、アセンブリ言語のプログラムでも、他のどんなプログラミング言語で書いたプログラムでも、自分で考えながら書いたプログラムが一発で意図した通りに動作する事は少ないでしょう。 余程簡単なプログラムなら話は別ですが、熟練したプログラマーであっても一発で意図した通りに動作する事は少ないと思います。 作成したプログラムの動作確認をし、どこに問題があるのかをじっくり考え、プログラムを修正する。 意図した通りに動作するようになるまで、これを繰り返さなければなりません。

バージョン2.4g以降のHSPには、デバッグウインドウが搭載されています。 このデバッグウインドウを利用して変数の内容をチェックする事で、スクリプトのどこに問題があるのかが分かりやすくなります。 しかし、このデバッグウインドウでチェックする事ができる変数は、HSPの内部で使用されている変数や、スクリプトが使用している変数だけです。 プラグインの内部で使用している変数や、レジスタの内容をチェックする事はできません。 そこで、レジスタの内容をチェックする事ができる「デバッグウインドウもどき」を作成してみました。 この「デバッグウインドウもどき」を利用する事で、MASM32で作成したプログラムのデバッグが比較的容易に行えるようになります。 MASM32で作成したプログラムが意図した通りに動作しない時は、このデバッグウインドウもどきを利用してみてください。 恐らく、MASM32で作成したプログラムであればHSP用のプラグイン以外でも利用できると思います。

5.2 間違ったプログラムの例

以下のプログラムを見てください。 一見、第1章に掲載したASMHSP.ASMのように見えますが、わざと間違えて書いてあります。 第1章に掲載したプログラムではASMHSP.ASを実行すると「123」と表示されましたが、間違ったプログラムを実行してみると「23」と表示されてしまいます。 「この程度のプログラムなら、デバッグウインドウもどきなんか使わなくてもプログラムをよく見直せば分かるじゃないか」、というツッコミは無しにして、次の節ではデバッグウインドウもどきの使い方を説明します。

ASMHSP.ASM(間違ったプログラム)
.486
.model          flat,stdcall
.code

DLLMain         proc    p1,p2,p3
                mov     eax,1
                ret
DLLMain         endp

asmhsp          proc    export p1:ptr,p2,p3,p4
                push    esi
                mov     esi,p1
                mov     eax,p2
                add     eax,p3
                add     ecx,p4
                mov     [esi],eax
                mov     eax,0
                pop     esi
                ret
asmhsp          endp

end             DLLMain
ASMHSP.AS(第1章のサンプルと同じ)
#uselib "asmhsp.dll"
#func asmhsp asmhsp 1

asmhsp p1,3,20,100
print p1
stop

5.3 デバッグウインドウもどきの使い方

まず、DEBUG.ASMをインクルードします。 この時、第4章のサンプルのようにWin32APIを利用している場合は、他のインクルードファイルよりも後でインクルードしてください。 (理由は次の節の最初に説明します。)

次に、レジスタの内容をチェックしたい所でdispregを呼び出します。 dispregに渡す引数は適当な値で構いませんが、複数の箇所から呼び出す場合はそれぞれ違う値にしておいてください。 この値は、デバッグウインドウもどきに行番号として表示されます。 繰り返しや分岐などで複雑になったプログラムをデバッグする時に、どこから呼び出されたのか区別するために用います。

ASMHSP.ASM(デバッグ用)
.486
.model          flat,stdcall
.code

include         debug.asm

DLLMain         proc    p1,p2,p3
                mov     eax,1
                ret
DLLMain         endp

asmhsp          proc    export p1:ptr,p2,p3,p4
                push    esi
                invoke  dispreg,1
                mov     esi,p1
                invoke  dispreg,2
                mov     eax,p2
                invoke  dispreg,3
                add     eax,p3
                invoke  dispreg,4
                add     ecx,p4
                invoke  dispreg,5
                mov     [esi],eax
                invoke  dispreg,6
                mov     eax,0
                invoke  dispreg,7
                pop     esi
                invoke  dispreg,8
                ret
asmhsp          endp

end             DLLMain

それでは早速、誤ったプログラムの動作を確認してみましょう。

これは、esiレジスタの値をpushした直後の状態です。

lineの後ろの数値は、dispregを呼び出す時に渡された引数です。 EAX, EBX, ECX, EDX, ESI, EDIは、見ての通りレジスタの中身です。 これらは、16進数と10進数で表示されます。

Carry flag, Parity flag, Zero flag, Sign flag, Overflow flagは、フラグレジスタの各ビットの値を表しています。

繰り返しを含むプログラムの場合、プログラムに問題があると無限ループに陥ってしまう事があります。 また、アセンブリ言語のプログラムでは、繰り返しを含まなくてもプログラムに問題があると暴走する場合があります。 例えば、push命令とpop命令がきちんと対応していないとret命令を実行した時に暴走します。 このような時、普通はCtrl+Alt+Deleteで強制終了しますが、「いいえ」をクリックする事でプログラムの実行を中断する事ができます。



これは、esiレジスタにp1のポインタを代入した直後の状態です。 ちゃんとesiレジスタの内容が変化しているのが分かります。



これは、eaxレジスタにp2の値を代入した直後の状態です。 eaxレジスタの値は元々3なので分かりにくいですが、ちゃんとp2の値(3)が代入されています。



これは、eaxレジスタにp3の値を加算した直後の状態です。 ちゃんとp3の値(20)が加算されているのが分かります。



これは、eaxレジスタにp4の値を加算した直後の状態です。 ちゃんとp4の値(100)が加算されているのが分かります。

と言いたいところですが、eaxレジスタの値は変化していませんね。 なぜかecxレジスタの値が変化しています。 そこで、プログラムをよく見てみると・・・ eaxではなくecxにp4の値を加算しています。 この部分が間違っている事が分かりますね。



esiレジスタが指しているアドレスに、eaxレジスタの値を代入した直後の状態です。 この時、レジスタの値は変化しません。



eaxレジスタに0を代入した直後の状態です。 確かにeaxレジスタの値が0になっています。



esiレジスタの値を復元した直後の状態です。 確かにesiレジスタの値が復元されています。



といった具合に、デバッグウインドウもどきを利用する事で細かい動作をチェックする事ができます。 今回は説明の為に一つ一つの命令の後でdispregを呼び出していますが、実際にはそこまでする必要はありません。 何度も呼び出さなくても、怪しそうな箇所から呼び出すだけでOKです。 何度も「はい」をクリックするのは面倒ですから。

5.4 デバッグウインドウもどきのプログラム

さて、デバッグウインドウもどきのプログラム(DEBUG.ASM)の中身について説明しましょう。 まず、「ifndef MessageBoxA」というのが出てきます。 これは、「endif」までの間の命令を、「MessageBoxA」が定義されていないときだけアセンブルしなさい、という命令です。 「MessageBoxA」が既に定義されている時は、endifまでの間の命令は無視されます。 このようにすることで、同じファイルを2回インクルードしてしまうのを防いでいます。 前の節の最初に「他のインクルードファイルよりも後でインクルードしてください。」と書いたのはこのためです。 「ifndef ExitProcess」から「endif」までの間も同様に、「ExitProcess」が既に定義されている時は無視されます。

;user32.incがインクルードされていなければインクルードする
ifndef		MessageBoxA
include		\masm32\include\user32.inc
includelib	\masm32\lib\user32.lib
endif

;kernel32.incがインクルードされていなければインクルードする
ifndef		ExitProcess
include		\masm32\include\kernel32.inc
includelib	\masm32\lib\kernel32.lib
endif

.dataから.codeまでの間については、説明すると長くなりそうなのでここでは敢えて説明しません。 本当は結構重要な部分なのですが、今回は読み飛ばしてください。(^^;

itoa16は、2番目の引数(src)の値を文字列(8桁の16進数)に変換し、1番目の引数が指しているアドレスに格納するプロシージャです。 HSPのスクリプトならstr命令を利用すれば簡単に変換できますが、ここではstr命令は利用できないので自前で変換しなければなりません。 まず、srcの値と0fhとの論理和(AND)をとる事で一番下の桁の値を求めます。 すると、0〜15(0〜0fh)の値になります。 この値が0〜9ならば'0'(0の文字コード)を加え、10〜15(0ah〜0fh)ならば'A'-10を加える事で、一番下の桁が変換できます。 後は、4ビットシフトしてこれを繰り返す事で各桁の値を変換する事ができます。 shrというのが右にシフトする命令です。

stosbというのは、ediレジスタが指しているアドレスにalレジスタの値を格納し、ediレジスタの値を増減させる命令です。 stosbを実行するたびにediレジスタの値を1増やしたい時はcld命令を実行しておき、stosbを実行するたびにediレジスタの値を1減らしたい時はstd命令を実行しておきます。 これについては、後の章で詳しく説明します。

loop命令は、見ての通りループ命令ですが、HSPのloop命令とは使い方が異なります。 loop命令が実行されると、まずecxレジスタの値が1減らされます。 そして、ecxレジスタの値が0以外の値になった時は指定されたラベルにジャンプします。 ecxレジスタの値が0になった時はジャンプせず、loop命令の次の命令が実行されます。 つまり、繰り返したい回数をecxレジスタに代入しておき、loop命令でその回数だけ繰り返すわけです。 但し、ecxレジスタに0をいれておけば0回繰り返されるというわけではないので注意してください。 ecxレジスタの値が0の時にloop命令が実行されると、ecxレジスタの値は0ffffffffh(4294967295)になってしまいます。 従って、4294967296回も繰り返されてしまう事になります。

今述べた手順で変換をするのが左下のプログラムですが、DEBUG.ASMではもっと効率の良い方法で変換しています。 それが右下のプログラムです。 sbbやdasといった見慣れない命令が出てきますが、普段はあまり使わない命令なので覚えておく必要はありません。 これは、僕が自分で考えた方法ではなく、Mr.参謀(堀籠 隆)氏のMASMライブラリ(DISPREG.SUBの中の_REG100)を参考にしました。

ちなみに、proc命令の後ろには「export」と書く代わりに「private」と書いてあります。 これは、このプロシージャがHSPから直接呼び出されるプロシージャではなく、DLLの内部からのみ呼び出されるプロシージャである事を示しています。

比較的分かりやすい方法
itoa16          proc    private dst:ptr,src
                std
                mov     ebx,src
                mov     ecx,8
                mov     edi,dst
                add     edi,7
        @@:     mov     al,bl
                and     al,0fh
                cmp     al,0ah
                jae     L1
                add     al,'0'          ;0〜9なら'0'を加える
                jmp     L2
        L1:     add     al,'A'-10       ;10〜15(0ah〜0fh)なら'A'-10を加える
        L2:     stosb
                shr     ebx,4
                loop    @b
                ret
itoa16          endp
効率的な方法
itoa16          proc    private dst:ptr,src
                std
                mov     ebx,src
                mov     ecx,8
                mov     edi,dst
                add     edi,7
        @@:     mov     al,bl
                and     al,0fh          ; 00h - 09h / 0Ah - 0Fh
                cmp     al,0ah          ; CY        / NC
                sbb     al,69h          ; 96h - 9Fh / A1h - A6h
                das                     ; 30h - 39h / 41h - 46h
                stosb
                shr     ebx,4
                loop    @b
                ret
itoa16          endp

itoa10は、2番目の引数(src)の値を文字列(11桁の10進数)に変換し、1番目の引数が指しているアドレスに格納するプロシージャです。 これもHSPのスクリプトならstr命令を利用すれば簡単に変換できますが、ここではstr命令は利用できないので自前で変換しなければなりません。 どのようにして変換すれば良いか考えてみましょう。

まず、先程と同様に1の位から求める方法が考えられます。 srcの絶対値を10で割ると、その余りが1の位の値になり、商をさらに10で割ると、その余りが10の値になります。 10で割るのを繰り返していけば、各桁の値が求まります。 この方法でも良いのですが、1の位から計算しているので表示させたい順番とは逆になっています。 従って、順番を並べ変えなければなりません。 そこで、上の桁から順に求める方法を考えてみましょう。

srcの値は32ビットの整数ですから、値の範囲は-2,147,483,648〜2,147,483,647です。 従って、符号を除くと最大で10桁になりますね。 ということは、srcの絶対値を1,000,000,000で割れば、商が1,000,000,000の位の値になります。 その余りを100,000,000で割れば、商が100,000,000の位の値になります。 その余りを10,000,000で割れば、商が10,000,000の位の値になります。 これを繰り返していけば、各桁の値を求める事ができます。 でも、例えばsrcの値が10000だったとすると、「0000010000」なんて表示されて欲しくないですよね? 「10000」と表示されて欲しいです。 そこで、0でない桁がまだ現れていない時は0を付けないようにしています。

「cmp edx,0」、「jge @f」で、srcの値が0以上ならば@@へジャンプします。 「neg edx」で、edxの符号を反転(-1倍)します。

先程、itoa16でstosbという命令を使いました。 このitoa10でも使っていますが、stosbの前にrepが付いています。 このようにrepを付けることで、stosb命令が繰り返されます。 繰り返しの回数はloop命令と同様ecxレジスタに入れておくのですが、repの場合ecxレジスタの値が0の時はstosbが実行されません。 また、rep命令はどんな命令の前にでも付けられるわけではなく、stosb等の限られた命令の前にだけ付ける事ができます。 (後の章で詳しく説明します。)

itoa10sub       proc    private                 ;各桁の値を計算するサブルーチン
                mov     eax,edx
                xor     edx,edx
                div     ebx                     ;eax=eax/ebx , edx=eax mod ebx
                cmp     al,0
                jnz     @f
                cmp     esi,1
                jz      @f
                ret                             ;0でない桁が現れていなければret
        @@:     add     al,'0'
                stosb
                dec     ecx
                mov     esi,1
                ret
itoa10sub       endp

itoa10          proc    private dst:ptr,src
                cld
                mov     ecx,11                  ;変換後の文字数
                mov     edx,src
                mov     edi,dst

                cmp     edx,0                   ;負の数の処理
                jge     @f
                mov     byte ptr [edi],'-'
                dec     ecx
                neg     edx
                inc     edi

        @@:     xor     esi,esi                 ;0でない桁が現れるまでesi=0にしておく
                mov     ebx,1000000000
                call    itoa10sub
                mov     ebx,100000000
                call    itoa10sub
                mov     ebx,10000000
                call    itoa10sub
                mov     ebx,1000000
                call    itoa10sub
                mov     ebx,100000
                call    itoa10sub
                mov     ebx,10000
                call    itoa10sub
                mov     ebx,1000
                call    itoa10sub
                mov     ebx,100
                call    itoa10sub
                mov     ebx,10
                call    itoa10sub
                add     dl,'0'
                mov     [edi],dl
                dec     ecx
                inc     edi

                mov     al,' '                  ;余った桁をスペースで埋めてret
                rep     stosb
                ret
itoa10          endp

itoa16, itoa10sub, itoa10では、レジスタの値を退避していません。 dispregの最初に退避しているので、itoa16, itoa10sub, itoa10では退避する必要がないからです。 また、dispregではpushf命令でフラグレジスタの値も退避しています。

setc, setp, setz, sets, seto命令は、普段はあまり使わない命令なので覚えておく必要はありません。 setcは、フラグレジスタの中のキャリーフラグの値をオペランドに代入する命令です。 setpは、フラグレジスタの中のパリティフラグの値をオペランドに代入する命令です。 setzは、フラグレジスタの中のゼロフラグの値をオペランドに代入する命令です。 setsは、フラグレジスタの中のサインフラグの値をオペランドに代入する命令です。 setoは、フラグレジスタの中のオーバーフローフラグの値をオペランドに代入する命令です。

第4章で説明したinvoke命令を用いて、itoa16とitoa10を呼び出しています。 「offset line16」というのは、line16という変数のアドレスを表しています。 「offset」を付けずに単に「line16」と書いてしまうとline16という変数の中身を渡す事になってしまいます。

MessageBoxAというのが、メッセージボックスを表示するWin32APIです。 これは、HSPのdialog命令に相当します。 MessageBoxAについては、ちょくと氏のHPを参照してください。 「HSPの裏技??」→「メッセージボックスを表示させてみる」に、このWin32APIを使ったサンプルスクリプトが掲載されています。

メッセージボックスの「はい」がクリックされると、MessageBoxAから6が返ってきます。 また、「いいえ」がクリックされると7が返ってきます。 そこで、7が返ってきたらExitProcessを呼び出し、プログラムを終了します。 このExitProcessは、プログラムを終了させるWin32APIです。

dispreg         proc    private line
                push    eax
                push    ebx
                push    ecx
                push    edx
                push    esi
                push    edi
                pushf
                setc    cf
                setp    pf
                setz    zf
                sets    sf
                seto    of
                add     cf,'0'
                add     pf,'0'
                add     zf,'0'
                add     sf,'0'
                add     of,'0'
                invoke  itoa16,offset line16,line
                invoke  itoa10,offset line10,line
                invoke  itoa16,offset  eax16,[ebp- 4]
                invoke  itoa10,offset  eax10,[ebp- 4]
                invoke  itoa16,offset  ebx16,[ebp- 8]
                invoke  itoa10,offset  ebx10,[ebp- 8]
                invoke  itoa16,offset  ecx16,[ebp-12]
                invoke  itoa10,offset  ecx10,[ebp-12]
                invoke  itoa16,offset  edx16,[ebp-16]
                invoke  itoa10,offset  edx10,[ebp-16]
                invoke  itoa16,offset  esi16,[ebp-20]
                invoke  itoa10,offset  esi10,[ebp-20]
                invoke  itoa16,offset  edi16,[ebp-24]
                invoke  itoa10,offset  edi10,[ebp-24]
                invoke  MessageBoxA,0,offset dispregbuf,offset dispregtitle,24h
                cmp     eax,6
                jz      @f
                invoke  ExitProcess,0
        @@:     popf
                pop     edi
                pop     esi
                pop     edx
                pop     ecx
                pop     ebx
                pop     eax
                ret
dispreg         endp

5.5 おわりに

ざっと説明しましたがいかがでしたでしょうか? DEBUG.ASMの中身について解説する前に解説すべき事は沢山あったかと思いますが、これを読んでくれている方がご自分で組んだプログラムをデバッグする際のお役に立てばと思い、先にDEBUG.ASMを掲載させていただきました。 この章で説明を省略した部分については、後の章で説明していきたいと思います。