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;
}

このソースコードをコンパイルして作成されたリスティングファイル( .cod )は以下のようになります。

(…………………… 省   略 ……………………)
PUBLIC  _EnumWindowsProc@8
;       COMDAT _EnumWindowsProc@8
_TEXT   SEGMENT
_hwnd$ = 8
_lParam$ = 12
_EnumWindowsProc@8 PROC NEAR                            ; COMDAT
; File C:\My Documents\C_obj\Hspmcn\enumwnd2.c
; Line 15
  00000 8b 44 24 08      mov     eax, DWORD PTR _lParam$[esp-4]
  00004 8b 48 08         mov     ecx, DWORD PTR [eax+8]
  00007 8b 50 04         mov     edx, DWORD PTR [eax+4]
  0000a 3b ca            cmp     ecx, edx
  0000c 7c 05            jl      SHORT $L73115
; Line 16
  0000e 33 c0            xor     eax, eax
; Line 20
  00010 c2 08 00         ret     8
$L73115:
; Line 17
  00013 8b 10            mov     edx, DWORD PTR [eax]
  00015 56               push    esi
  00016 8b 74 24 08      mov     esi, DWORD PTR _hwnd$[esp]
  0001a 89 34 8a         mov     DWORD PTR [edx+ecx*4], esi
; Line 18
  0001d 8b 48 08         mov     ecx, DWORD PTR [eax+8]
  00020 41               inc     ecx
  00021 5e               pop     esi
  00022 89 48 08         mov     DWORD PTR [eax+8], ecx
; Line 19
  00025 b8 01 00 00 00   mov     eax, 1
; Line 20
  0002a c2 08 00         ret     8
_EnumWindowsProc@8 ENDP
_TEXT   ENDS
END

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

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

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

sdim cod, 10000
; 出力されたマシン語コード
cod = {"
 8b 44 24 08
 8b 48 08
 8b 50 04
 3b ca
 7c 05
 33 c0
 c2 08 00
 8b 10
 56
 8b 74 24 08
 89 34 8a
 8b 48 08
 41
 5e
 89 48 08
 b8 01 00 00 00
 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, 12
fncode.0 = $0824448b, $8b08488b, $ca3b0450, $c033057c, $8b0008c2, $748b5610
fncode.6 = $34890824, $08488b8a, $48895e41, $0001b808, $08c20000, $00000000

実際に使ってみる

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

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

    ; コールバック関数のマシン語コードを変数に格納
    xdim fncode, 12
    fncode.0 = $0824448b, $8b08488b, $ca3b0450, $c033057c, $8b0008c2, $748b5610
    fncode.6 = $34890824, $08488b8a, $48895e41, $0001b808, $08c20000, $00000000

    ; ウィンドウハンドルを格納する配列変数
    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

前回よりもすっきりした感じがします。安全性も、前回よりずっと高まってます。