EnumWindows でのウィンドウ列挙

まずは、 EnumWindows 関数を使用してウィンドウハンドルを列挙する場合に使用される、 EnumWindowsProc コールバック関数のコードを作成してみます。

HSP スクリプト中で呼び出す EnumWindows 関数は以下のように定義されています。

BOOL EnumWindows(
    WNDENUMPROC lpEnumFunc,  // コールバック関数のアドレス
    LPARAM      lParam       // アプリケーション定義値
);

この関数の lpEnumFunc パラメータに指定するものは、アプリケーション側で実装された EnumWindowsProc コールバック関数のアドレスになります。

BOOL CALLBACK EnumWindowsProc(
    HWND   hwnd,      // ウィンドウハンドル
    LPARAM lParam     // アプリケーション定義値
);

アプリケーションが EnumWindows 関数を呼び出したとき、存在するトップレベルウィンドウの数だけ EnumWindowsProc コールバックが呼び出されるようになっています。このとき、コールバック関数の hwnd パラメータに、それぞれのトップレベルウィンドウのハンドルが渡されています。また、今回は使用しませんが、 EnumWindowsProclParam パラメータには、 EnumWindows 関数の lParam に指定された値が格納されるようになっています。

コールバック関数のソースの作成

通常、(HSPのマシン語コード用としてではなく)Cで書く場合には、ソースプログラムの1つの例としては以下のようになると思います。この例では、グローバル変数として確保された配列変数にウィンドウハンドルを格納していくことになります。

#include <windows.h>

#define MAX_NUM_ENUMWND  520     // 列挙するウィンドウの最大数

HWND g_hwnd[MAX_NUM_ENUMWND];    // 格納する配列変数
int  g_cnt = 0;                  // カウンタ

BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam)
{
    // 最大数を超えた場合は列挙を終了させる
    if (g_cnt >= MAX_NUM_ENUMWND)
        return FALSE;
    // 配列変数に格納
    g_hwnd[g_cnt] = hwnd;
    g_cnt++;
    return TRUE;
}

もちろん、このプログラムでは EnumWindows 関数呼び出し前に、カウンタ変数を 0 に初期化する必要がありますが。


さて、これをHSPで使用されるマシン語コード用に書き換えてみましょう。

プログラム中では外部変数/静的変数は使用せずに、 HSP 側で確保された変数を使用します。ここでは、ウィンドウハンドルを格納する配列変数およびカウンタ変数を HSP 側の変数であるとし、プログラム中ではその変数を指すポインタを使用します。ソースプログラムは以下のようになります。

ソースファイル enumwnd.c

#include <windows.h>

BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam)
{
    HWND *volatile p_hwnd = (HWND *)0x11111111;   // 配列変数アドレス
    int  *volatile p_cnt  = (int *)0x22222222;    // カウンタ変数アドレス
    volatile int max_num = 0x33333333;            // 最大数

    if (*p_cnt >= max_num)
        return FALSE;
    p_hwnd[*p_cnt] = hwnd;
    (*p_cnt)++;
    return TRUE;
}

ウィンドウハンドル用配列変数やカウンタ用変数のアドレスは、実際にはスクリプト実行時まで分かりません。従って、とりあえずは上のように 0x11111111 などのような、後で見て一目でわかる数値をキャストして入れておきます。ここでは、取得できる最大数も変えられるように、上のようにしておきます。

ここで、後から値を埋め込む変数やポインタの宣言時には、最適化の防止を行なう修飾子 volatile をできるだけ付けるようにしましょう。そうしないと、コンパイル時に変数への最適化が行なわれて、マシン語コードの中に、データを埋め込まなければならない部分がたくさん出てきてしまうこともあります。

また、ポインタ宣言時には、 volatile を記述する位置にも注意する必要があります。もしも

volatile HWND *p_hwnd = (HWND *)0x11111111;

というようにしたり、あるいは、同じことですが、

HWND volatile *p_hwnd = (HWND *)0x11111111;

としたりすると、これは、ポインタが指す変数(すなわち (*p_hwnd) )の内容に対しての最適化の防止を指示していることになってしまいます。しかし、今回の場合は、アドレスがコード中に分散して出てこないよう、ポインタそのものに対して最適化抑止する必要があるので、

HWND *volatile p_hwnd = (HWND *)0x11111111;

と記述しなければならないわけです。

ただし、今回に限っては、すべての変数に volatile を付けないでコンパイルしました。今回の場合は、これらの情報がマシン語コードの中で複数に分散して出てくることはないと筆者が判断したためです。今回のように短いコードの場合は、付けるのと付けないのとでは、最終的なマシン語のサイズが大きく変わってきてしまうので、状況に応じて判断する必要があります。

コンパイル

完成したらこのソースプログラムをコンパイルしますが、このときに、オプション指定でマシン語を含むリスティングファイルを出力するように指定しておきます。 VC++ 6.0 の場合は、

  1. 「プロジェクト」メニュー
  2. 「設定」
  3. 「C/C++」タブ
  4. カテゴリ「ファイルリスティング」
  5. リスティングファイルタイプ「マシン語コードを含む」を選択

という順序で設定できます。

ファイルをコンパイルすると、設定されているディレクトリ(デフォルトは「Release」ディレクトリ)にリスティングファイル(拡張子 .cod )ができていると思います。 VC++ では、以下のようなファイルが作成されます。(コンパイラの種類やバージョンのほか、最適化などの設定によっても異なります。)

(…………………… 省   略 ……………………)

PUBLIC  _EnumWindowsProc@8
;       COMDAT _EnumWindowsProc@8
_TEXT   SEGMENT
_hwnd$ = 8
_EnumWindowsProc@8 PROC NEAR                            ; COMDAT
; File C:\My Documents\C_obj\Hspmcn\Enumwnd.c
; Line 6
  00000 b8 22 22 22 22   mov     eax, 572662306         ; 22222222H
; Line 9
  00005 8b 08            mov     ecx, DWORD PTR [eax]
  00007 81 f9 33 33 33
        33               cmp     ecx, 858993459         ; 33333333H
  0000d 7c 05            jl      SHORT $L73112
; Line 10
  0000f 33 c0            xor     eax, eax
; Line 14
  00011 c2 08 00         ret     8
$L73112:
; Line 11
  00014 8b 54 24 04      mov     edx, DWORD PTR _hwnd$[esp-4]
  00018 89 14 8d 11 11
        11 11            mov     DWORD PTR [ecx*4+286331153], edx
; Line 12
  0001f 8b 08            mov     ecx, DWORD PTR [eax]
  00021 41               inc     ecx
  00022 89 08            mov     DWORD PTR [eax], ecx
; Line 13
  00024 b8 01 00 00 00   mov     eax, 1
; Line 14
  00029 c2 08 00         ret     8
_EnumWindowsProc@8 ENDP
_TEXT   ENDS
END

上の、色つき表示されている部分では、左側がコードの先頭からのオフセットを、右側の部分がマシン語コードをそれぞれ示しています。これらはどちらも16進数表示になっていることに注意してください。スクリプトを実行した時にマシン語コードがメモリ上にこのように配置されるように、配列変数に値を格納しなければなりません。


今回はコードの量が少ないですし、今回が初めてであるということもあるので、配列変数に直接書き込んでいくことにします。ただし、上の「11 11 11 11」などの部分は、後でスクリプト上で別の値(変数アドレスなど)を格納するためのものなので、この部分は 0 などでもかまいませんが。

さて、 Windows のメモリ管理は「リトルエンディアン方式」と呼ばれ、下位バイトのほうが先に格納されるようになっています。すなわち、32ビット数値 0x12345678 は、メモリ上で「78 56 34 12」の順で格納されるのです。従って、

  00000  b8 22 22 22 22
  00005  8b 08
  00007  81 f9 33 33 33
         33
  0000d  7c 05
  0000f  33 c0

となっている部分のマシン語コードを配列変数に格納するには、まず、以下のように並べて、これを4バイトずつに区切ってみます。

b8 22 22 22 / 22 8b 08 81 / f9 33 33 33 / 33 7c 05 33 / c0 …

それをバイトごとに逆転させて格納します。

fncode.0 = $222222b8
fncode.1 = $81088b22
fncode.2 = $333333f9
fncode.3 = $33057c33

これでマシン語がメモリ上に格納できたわけです。 EnumWindows 関数呼び出し時に、コールバック関数のアドレスが必要になりますが、これはこのマシン語の先頭のアドレス、すなわち、変数 fncode のアドレスを指定すればいいのです。

さて、これで、そのまま EnumWindows 関数を呼び出してウィンドウハンドルが取得できるかというと、そうではありません。上のマシン語コード上で「11 11 11 11」などとなっていた部分に正しい値を格納しなければなりません。それぞれの数値のオフセットを考えて、 memcpyll_poke4 などで正しい値を格納しましょう。上のコードの場合は、以下のようになります。

オフセット 仮の値 実際の値
1 22222222 カウンタ変数のアドレス
9 33333333 列挙する最大数(=配列変数の要素数)
$1b (27) 11111111 ハンドルを格納する配列変数のアドレス

以上から、全体のスクリプトは次のようになります。

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

    ; マシン語を変数に格納する (必ず xdim を使用すること)
    xdim fncode, 11
    fncode   = $222222b8, $81088b22, $333333f9, $33057c33, $0008c2c0, $0424548b
    fncode.6 = $118d1489, $8b111111, $08894108, $000001b8, $0008c200

    ; マシン語コードのアドレス(コールバック関数アドレス)取得
    getptr pfn, fncode

    ; カウンタ用変数のアドレスを1バイト目にセット
    getptr t, wndcnt
    memcpy fncode, t, 4, 1

    ; 列挙するウィンドウの最大数(配列の要素数)を9バイト目にセット
    t = 256
    memcpy fncode, t, 4, 9
    ; ウィンドウハンドルを格納する配列変数のアドレスを$1bバイト目にセット
    dim hwnd, 512
    getptr t, hwnd
    memcpy fncode, t, 4, $1b

    ; カウンタを 0 にセット
    wndcnt = 0

    ; EnumWindows の呼び出し
    pm = pfn, 0
    dllproc "EnumWindows", pm, 2, D_USER

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