Turbo Debugger for Win32を使ってみる


Turebo Debugger 5.5(以下TD32)はここから ダウンロードできる. 基本的にフリーで使用できるデバッガである. Delphi IDEには使いやすいデバック機能があるが,今までスタンドアローンの デバッガを使ったことが無かったのでちょっと触ってみた. 1996年出版のDelphi 32ビットパワープログラミング(インプレス社)には TD32の使い方が簡単に書かれているので参考にした.

TD32でデバッグするには実行イメージにTD32用のデバッグ情報を埋め込まなければならない. それには,プロジェクトオプションの「リンカ」ペインでTD32デバッグ情報を含めるを チェックする. また,「コンパイラ」ペインの「コード生成」で最適化のチェックを外しておく. 最適化により監視したい変数が削除されたりするのを防ぐためである.

上記のパワープログラミングによると,この設定でコンパイルすればTD32でデバッグできるはずなのだが, なぜか変数がwatchできなかった. 原因としては,やり方が悪いか,D5では何か別の設定が必要(パワープログラミングはD2向けに書かれている), のどちらかであろう. たぶん前者だとう思うが.

IDEからコンパイルした実行バイナリはTD32でデバッグできなかったが,とりあえず コマンドラインコンパイラdcc32を 使えばTD32でデバッグ可能な実行バイナリを作成することができた. そのためには,ソースコードの冒頭に最適化を抑制するスイッチ{$O-}を付けておき,

> dcc32 -V tdtest.pas

のようにTD32のデバッグ情報を含めるための-Vオプションを指定してコンパイルすればよい.

デバッグしてみたコードは,件のパワープログラミングに書かれている コードを一部改変した以下のようなものである.

program Tdtest;
{$O-}

uses
  SysUtils;

var
  A: Pointer;
  B: PChar;

begin
   A := Ptr( HInstance );         // HInstanceをポインタとして代入
   B := Ptr( $000045FF );         // でたらめなアドレスを代入
   A := nil;                      // Aをnilとする
   B := nil;                      // Bをnilとする
   GetMem( A, 100 );              // 100バイト割り当て
   GetMem( B, 100 );              // 100バイト割り当て
   StrCopy( B, 'Hello World!' );  // 割り当てたメモリに文字列をコピー
   FreeMem( A, 100 );             // 割り当てたメモリを解放
   A := nil;                      // Aをnilとする
   FreeMem( B, 100 );             // 割り当てたメモリを解放
   asm                            // asmブロック開始
     mov longint ptr B, 0;        // Bを0に
   end;                           // asmブロック終了
end.

なお,上記コードをコンパイルする際は,

> dcc32 -CC -V tdtest.pas

のようにコンソールプログラムをターゲットとする-CCオプションを付けた(ただし,標準入出力等が 含まれていないのでこのオプションは無くてもよい).

TD32で実際にデバッグしてみる. TD32はDOSベースのアプリケーションであるが,Win2Kのコンソールでも正常に動作する. ただし,そのままではマウスが使えないので,一旦TD32を起動してウィンドウ左上の システムメニューからプロパティを開き,「オプション」ペインの簡易編集モードの チェックを外しておく.これでマウスが使えるようになる. さらに,デフォルトでは画面バッファが大きすぎてwatchウィンドウ等が画面からはみ出してしまうので 「レイアウト」ペインの 画面バッファのサイズで高さを25程度にしておくとよいようだ. 以下はTD32を起動し,上でコンパイルしたtdtest.exeをロードしたところである.

"View" → "Watches" を選ぶとwatchウィンドウが画面下部に表示される. "Data" → "Add watch..." を選択し,A,B,@A,@B,A^ および B^ をwatchウィンドウに追加した.

F8を押すと実行が始まる. 以下は1行目の実行が終了した後のwatchウィンドウである.

B^                           ?? : CHAR
A^                           @:00400000 : UNTYPED
@B                           :0040A604 : POINTER
@A                           :0040A600 : POINTER
B                            :00000000 : PCHAR
A                            :00400000 : POINTER

ポインタAには$00400000が格納されている. $00400000はグローバル変数hInstanceの値であり,実行バイナリのベースアドレスを示している. この値はプロジェクトオプションの「リンカ」ペインのイメージベースで設定されており, デフォルトがこの$00400000である.

すべての32bitアプリケーションは32bitのフラットなアドレス空間にマッピングされ, 4GBまでのメモリ空間を使うことができる(実際にはOSが半分を占有するので2G). $00400000を10進数に直すと4194304となり,これをMBに直せばちょうど4MBとなる. したがって,通常のアプリケーションは0番地のアドレスから4MB分オフセットしたアドレスに マッピングされることがわかる. なお,16進と10進の変換はWindows付属の電卓を使うと簡単だ.

AおよびBのアドレス(@Aおよび@B)を見ると4バイト分ずれている. ポインタは4バイトであるから,メモリ上のBはAとちょうど隣り合っていることになる.

"View" → "CPU" を選ぶと,CPUビューが開き 実行中のアセンブラコードおよびレジスタの中身を見ることができる. 以下はGetMemの呼び出しでCPUビューを表示させたところである.

GetMemの呼出しでは,確保するメモリサイズを表す整数値がeaxレジスタにプッシュされ($00000064は10進数 の100である),GetMemが呼ばれる. GetMemが終了すると確保したメモリの先頭アドレスがやはりeaxレジスタに入っているので, その値をアドレス0040A600,すなわちAにコピーしている.

その前のBにnilを代入しているところも面白い. まずeaxレジスタの値同士をxorする. これでeaxレジスタにどんな値が入っていてもレジスタの中身が$00000000になる(どんな数でもその数同士の xorは0になる)ので,それをアドレス0040A604,すなわちBにコピーしている.

AとBに対してGetMemが終わった後のwatchウィンドウは以下のようになった.

B^                           #0 : CHAR
A^                           @:00CE097C : UNTYPED
@B                           :0040A604 : POINTER
@A                           :0040A600 : POINTER
B                            :00CE09E4 : PCHAR
A                            :00CE097C : POINTER

AとBはいずれも100バイトの領域へのポインタとなっており,両者の差は104バイトである. 領域のサイズよりも4バイト多いのには何か意味がありそうだ.

次はStrCopyの呼び出しをCPUビューで見てみたところである.

まずedxレジスタに$00408684というアドレスがコピーされているが,これは実行バイナリ中で "Hello World!" の文字列 リソースが存在する場所だろう. 次にBのアドレス,すなわち$0040A604がeaxレジスタにコピーされStrCopyが呼び出されている. 呼び出し後のwatchウィンドウは

B^                           'H' : CHAR
A^                           @:00CE097C : UNTYPED
@B                           :0040A604 : POINTER
@A                           :0040A600 : POINTER
B                            :00CE09E4 : PCHAR
A                            :00CE097C : POINTER

のようになった. Bが先頭文字の 'H' を差していることがわかる.

その次のFreeMemの呼び出しであるが,まず,解放すべき領域へのポインタAの アドレス$0040A600がeaxレジスタにコピーされた後FreeMemが呼び出される. Bについても同様である. レジスタにコピーされるのは領域へのポインタのアドレスのみで, 領域のサイズはFreeMemの呼び出し前にレジスタにコピーされていない.

推測であるが,GetMemは確保したメモリ領域のサイズを,戻り値として返すアドレスの 負のオフセットに格納しておくのではあるまいか. GetMemで確保したAとBのアドレスの差が予測よりも4バイト多かったのは, Bの差す領域の負のオフセットに領域のサイズが格納されているのではないだろうか. 4バイトあればどのようなサイズでも格納できる. ヘルプにもFreeMemで解放するメモリはGetMemで確保したものに限るという意味のことが書かれている.

このあたりのことはドキュメント化されていないことで,いろいろと想像するとおもしろい.


お問い合わせはメールにて: akasaka@klc.ac.jp

戻る
SEO [PR] 爆速!無料ブログ 無料ホームページ開設 無料ライブ放送