HSPのスクリプトでも、アセンブリ言語のプログラムでも、他のどんなプログラミング言語で書いたプログラムでも、自分で考えながら書いたプログラムが一発で意図した通りに動作する事は少ないでしょう。 余程簡単なプログラムなら話は別ですが、熟練したプログラマーであっても一発で意図した通りに動作する事は少ないと思います。 作成したプログラムの動作確認をし、どこに問題があるのかをじっくり考え、プログラムを修正する。 意図した通りに動作するようになるまで、これを繰り返さなければなりません。
バージョン2.4g以降のHSPには、デバッグウインドウが搭載されています。 このデバッグウインドウを利用して変数の内容をチェックする事で、スクリプトのどこに問題があるのかが分かりやすくなります。 しかし、このデバッグウインドウでチェックする事ができる変数は、HSPの内部で使用されている変数や、スクリプトが使用している変数だけです。 プラグインの内部で使用している変数や、レジスタの内容をチェックする事はできません。 そこで、レジスタの内容をチェックする事ができる「デバッグウインドウもどき」を作成してみました。 この「デバッグウインドウもどき」を利用する事で、MASM32で作成したプログラムのデバッグが比較的容易に行えるようになります。 MASM32で作成したプログラムが意図した通りに動作しない時は、このデバッグウインドウもどきを利用してみてください。 恐らく、MASM32で作成したプログラムであればHSP用のプラグイン以外でも利用できると思います。
以下のプログラムを見てください。 一見、第1章に掲載したASMHSP.ASMのように見えますが、わざと間違えて書いてあります。 第1章に掲載したプログラムではASMHSP.ASを実行すると「123」と表示されましたが、間違ったプログラムを実行してみると「23」と表示されてしまいます。 「この程度のプログラムなら、デバッグウインドウもどきなんか使わなくてもプログラムをよく見直せば分かるじゃないか」、というツッコミは無しにして、次の節ではデバッグウインドウもどきの使い方を説明します。
|
|
まず、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です。 何度も「はい」をクリックするのは面倒ですから。
さて、デバッグウインドウもどきのプログラム(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の内部からのみ呼び出されるプロシージャである事を示しています。
|
|
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 |
ざっと説明しましたがいかがでしたでしょうか? DEBUG.ASMの中身について解説する前に解説すべき事は沢山あったかと思いますが、これを読んでくれている方がご自分で組んだプログラムをデバッグする際のお役に立てばと思い、先にDEBUG.ASMを掲載させていただきました。 この章で説明を省略した部分については、後の章で説明していきたいと思います。