EnumWindows でのウィンドウ列挙・再考編

今回やるのは、前回と同じく EnumWindows 関数を使用したウィンドウの列挙なのであります。

より安全なコードを目指して

前回は、アドレスや最大数などの情報を直接マシン語のバイナリコードの中に書き込んでいくということを行なっていましたね。これは、今のように小さな関数ならばそれほど問題ではないのですが、関数が大きくなるにつれて、これらの情報をどこに格納するのか(オフセット)などの情報が複雑になってくるでしょうし、ちょっとしたミスでプログラムが大暴走、ということも考えられます。

今回は、より安全にプログラミングを行なうということを考えてみましょう。で、どうすればいいのかというと、マシン語の中に直接埋め込むデータが少なければ少ないほどいいわけです。そこで今回は、HSPスクリプトから関数側に渡されるすべての情報をあらかじめ構造体(HSP側から見れば配列変数)に格納しておき、この構造体のアドレス1つだけを関数に渡す(埋め込む)ということにします。

しかも今回の場合は、 EnumWindows 関数の lParam パラメータに指定された値がそのまま EnumWindowsProclParam パラメータに渡されるので、これを構造体アドレスとすれば、マシン語コード内には何も埋め込まずに済みますよね。

ということで、今回は以下のよう ENUMWND_DATA 構造体を定義してみることにしましょう。

typedef struct {
    HWND *phwnd;    // ハンドルを格納する配列変数アドレス
    int   nmax;     // 最大数(配列の要素数)
    int   ncntr;    // カウンタ
} ENUMWND_DATA;

この構造体の実体は、HSP側で準備された配列変数になります。注意するべきことは、前回はカウンタ変数のアドレスを関数に埋め込んでいましたが、この構造体はメンバそのものがカウンタになっているという点です。

ソースの作成・マシン語コード抽出

コールバック関数のソースコードは以下のようになります。

ソースファイル enumwnd2.c

#include <windows.h>

// HSPからの情報を渡すための構造体
typedef struct {
    HWND *phwnd;    // ハンドルを格納する配列変数アドレス
    int   maxnum;   // 最大数(配列の要素数)
    int   cnt;      // カウンタ
} ENUMWND_DATA;

BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam)
{
    // lParam は ENUMWND_DATA 構造体アドレス
    ENUMWND_DATA *pData = (ENUMWND_DATA *)lParam;

    if (pData->cnt >= pData->maxnum)
        return FALSE;
    pData->phwnd[pData->cnt] = hwnd;
    pData->cnt++;
    return TRUE;
}

コンパイルについては前回とおんなじ。DOSプロンプト(コマンドプロンプト)を起動して、カレントディレクトリの移動、ソースのコンパイル、 TDUMP でマシン語情報の取り出しという手順をとります。

ただ、コンパイルのたびに、前回にやったようなコマンドラインをいちいち入力していくのは面倒ですよね。そこで、コンパイルとダンプ作業を行なうバッチファイルを作成して、作業を単純化させてみましょう。まず、以下のファイルを作成して、作業フォルダに mcnbcc.bat というファイル名で保存しておきます。

バッチファイル mcnbcc.bat

bcc32 -c -O1 %1.c
@if errorlevel == 1 goto END
tdump -oxCOMENT %1.obj %1.txt
start %1.txt
:END

上のバッチファイルの「%1」の部分が、パラメータとして指定された文字列に置き換えられて実行されるようになります。例えば、今回の場合、

mcnbcc.bat enumwnd2

または、コマンド入力時に拡張子『.bat』は省略できるので

mcnbcc enumwnd2

とすることで、「%1」の部分はすべて「enumwnd2」に置き換えられて実行されます。このとき、まず、ソースファイルのコンパイルを実行した後、コンパイラがエラーを返さなければ、TDUMPを実行し、最後にテキストファイル形式で出力されたダンプを開くようになっています。ファイルが自動的に開かれないようにしたい場合には、「start %1.txt」の行を削除してください。

実際の入出力は以下のようになります。

C:\WINDOWS>d:

D:\>cd hspmcn

D:\hspmcn>mcnbcc enumwnd2

D:\hspmcn>bcc32 -c -O1 enumwnd2.c
Borland C++ 5.5.1 for Win32 Copyright (c) 1993, 2000 Borland
enumwnd2.c:

D:\hspmcn>tdump -oxCOMENT enumwnd2.obj enumwnd2.txt
Turbo Dump  Version 5.0.16.12 Copyright (c) 1988, 2000 Inprise Corporation

D:\hspmcn>start enumwnd2.txt
D:\hspmcn>

これによって作成されたファイル enumwnd2.txt の内容は以下のようになっています。

(…………………… 省   略 ……………………)
000DF1 PUBDEF  'EnumWindowsProc'       Segment: _TEXT:0000
000E26 LEDATA  Segment: _TEXT          Offset: 0000  Length: 0028
    0000: 55 8B EC 53 8B 45 0C 8B  50 08 3B 50 04 7C 04 33   U駆S畿.輝.;P.|.3
    0010: C0 EB 10 8B 08 8B 5D 08  89 1C 91 FF 40 08 B8 01   タ....犠.....@.ク.
    0020: 00 00 00 5B 5D C2 08 00                            ...[]ツ..
000E55 MODE32

生成されたマシン語を変数に格納するのですが、前回のようにして直接手で打つのは、ミスによるバグを招く確率が非常に高いですね。今回はちょっと別の方法です。とりあえず、下のスクリプトを実行してみてください。

マシン語コード変換スクリプト

; マシン語コード変換スクリプト
#include "llmod.as"

varname = "fncode"       ; 変数名
numline = 5              ; 1行の要素数

sdim cod, 10000
; 出力されたマシン語コード
cod = {"
55 8B EC 53 8B 45 0C 8B  50 08 3B 50 04 7C 04 33
C0 EB 10 8B 08 8B 5D 08  89 1C 91 FF 40 08 B8 01
00 00 00 5B 5D C2 08 00
"}

; 文字列形式からバイナリコードに変換
dim bin, 800
ll_bin bin, cod
if stat : dialog "マシン語に変換できませんでした。", 1 : end
; dllret には変換サイズが格納されます
n = dllret + 3 / 4     ; バイナリコードの配列変数の要素数

dim buf, 10000
buf = "xdim "+varname+", "+n+"\n"
repeat
    if cnt == n : break
    if cnt \ numline == 0 : buf += varname+"."+cnt+" = "
    t = bin.cnt : str t, 24 : buf += "$"+t
    if (cnt+1\numline!=0)&(cnt+1!=n) : buf += ", " : else : buf += "\n"
loop
mesbox buf, winx, winy, 4
stop

上のスクリプトは、 loadlib.dll の ll_bin 命令を使用して、文字列形式で記述されているマシン語コードをバイナリ形式に変換し、それを HSP の変数に格納するためのスクリプトを作成します。このスクリプトを実行させると、以下のコードが取得できます。これをコピーしてそのままスクリプトに貼り付けてしまいましょう。

xdim fncode, 10
fncode.0 = $53ec8b55, $8b0c458b, $503b0850, $33047c04, $8b10ebc0
fncode.5 = $085d8b08, $ff911c89, $01b80840, $5b000000, $0008c25d

実際に使ってみる

HSPスクリプトは以下のようになります。今回はマシン語コード中に埋め込む情報はありません。代わりに、上で定義した ENUMWND_DATA 構造体となる配列変数に必要な情報を格納し、その変数のアドレスを EnumWindows 関数の lParam パラメータに渡さなくてはなりません。また、呼び出し前にはカウンタとなるメンバ(ここでは変数 ewdata.2)を 0 に初期化するのも忘れてはいけません。

    #include "llmod.as"
    #include "xdim.as"

    ; コールバック関数のマシン語コードを変数に格納
    xdim fncode, 10
    fncode.0 = $53ec8b55, $8b0c458b, $503b0850, $33047c04, $8b10ebc0
    fncode.5 = $085d8b08, $ff911c89, $01b80840, $5b000000, $0008c25d

    ; ウィンドウハンドルを格納する配列変数
    dim hwnd, 512

    ; ENUMWND_DATA 構造体
    getptr ewdata.0, hwnd       ; 配列変数のアドレス
    ewdata.1 = 512              ; 最大数(配列の要素数)
    ewdata.2 = 0                ; カウンタ(0 に初期化する)

    ; EnumWindows の呼び出し
    getptr pm.0, fncode     ; コールバック関数(マシン語コード)アドレス
    getptr pm.1, ewdata     ; ENUMWND_DATA 構造体アドレス
    dllproc "EnumWindows", pm, 2, D_USER

    dialog "" + ewdata.2 + "個のウィンドウハンドルが取得されました。"
    stop

前回よりもすっきりした感じがしますね。安全性も、前回よりずっと高まっているのであります。