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

コンパイル

完成したらこのソースプログラムをコンパイルしますが、BCCはコマンドラインコンパイラであるので、MS-DOSプロンプトから行なう必要があります。

まずは作業ディレクトリを決定し、そこにCで書いたソースファイルを保存しておきます。今回は、 ソースファイル『 D:\hspmcn\enumwnd.c 』として保存してあることにします。

次に、MS-DOSプロンプトを起動してから、作業ディレクトリに移ります。通常のWindowsプログラミングの場合と違って、MS-DOSの場合、「カレントドライブ」の概念があり、「カレントディレクトリ」はそれぞれのドライブに1つずつ割り当てられるので、ディレクトリ移動はドライブとディレクトリで別々に行なう必要があります。作業ディレクトリに移る場合、まず、プロンプトのカレントディレクトリのあるドライブが、作業ディレクトリのあるドライブと別のドライブであった場合には、半角文字で『(作業ドライブ名):』と入力して [Enter] キーを押します。例えば、Dドライブなら『D:』というようにします。ドライブを移したら、次に cd コマンドを使用して、ディレクトリを移動します。『cd (作業ディレクトリ名)』と入力して、[Enter]を押します。このときのディレクトリ名は、絶対パス(フルパス)で指定しても、カレントディレクトリからの相対パスで指定しても、どちらでもよいです。

今回の場合のDOSウィンドウの表示は以下のようになります。強調表示されている部分は、プログラマが入力したものになります。ここではディレクトリ名を相対パスで指定しています。

Microsoft(R) Windows 98
   (C)Copyright Microsoft Corp 1981-1998.

C:\WINDOWS>d:

D:\>cd hspmcn

D:\hspmcn>

BCCでのCコンパイラは、「bcc32.exe」という実行可能ファイルです。以下のようにすることで、コンパイルされてオブジェクトファイル(拡張子 .obj)が出来上がります。

bcc32 -c (ソースファイル名)

ここで、『-c』は、「コンパイルはするがリンクはしない」ということを指定するオプションパラメータです。(オプション指定は大文字・小文字が区別されるので注意。) 今回はマシン語を生成するのが目的であって、実行可能ファイルやDLLを作成するわけではないので、このオプションを指定しておく必要があります。

他にもいろいろとオプションを指定することができますが、ここでは詳しく説明しません。オプション指定についての細かい指定はBCCのリファレンスを参照してください。あえて言うなら、コードサイズ最適化オプション『-O1』(「ゼロ」ではなくて、アルファベットの大文字の「オー」です)や、速度最適化オプション『-O2』などがあります。

実際にコンパイルを行なうと、次のように表示されると思います。

D:\hspmcn>bcc32 -c -O2 enumwnd.c
Borland C++ 5.5.1 for Win32 Copyright (c) 1993, 2000 Borland
enumwnd.c:
警告 W8057 enumwnd.c 14: パラメータ 'lParam' は一度も使用されない(関数 EnumWindowsProc )

D:\hspmcn>

上の例では、最適化オプションなんかも指定していますが、指定しなくでも別にいいです。マシン語コードのサイズが気になる方は、コードサイズ最適化オプション『-O1』を指定してみるのがいいかもしれないです。

「パラメータ 'lParam' は一度も使用されない」という警告が表示されていますが、実際にソースコード内で使用していないので、特に気にする必要はありません。エラーがなければコンパイルはできたことになります。コンパイルに成功していれば、オブジェクトファイル『 enumwnd.obj 』が生成されているはずです。

マシン語コードを取り出す

出来上がったオブジェクトファイルにはマシン語コードがバイナリ形式で含まれているのでありますが、オブジェクトファイルの中身は構造が複雑なので、単純には取り出せません。

そこで、BCCに添付されている『TDUMP』というファイルダンプユーティリティを使います。このユーティリティは、オブジェクトファイルや実行可能ファイルをダンプして、構造を表示するというものです。これを使用して、オブジェクトファイルからマシン語コードの部分の情報を取り出します。TDUMP もコマンドラインツールなので、DOSプロンプト上で実行します。

tdump -oxCOMENT (オブジェクトファイル名) (出力ファイル名)

コマンドは上のようになります。オプション指定『-oxCOMENT』は、「COMENTと言う名前のレコードに含まれる情報は含めない」というものです。このレコードには、コメント情報のみが含まれていて、マシン語コードの情報は含まれていないにもかかわらず、この領域の情報は非常に量が多いので、あらかじめ取り除くように指定しておきます。

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

D:\hspmcn>

上の例では、ファイル『 enumwnd.txt 』に結果を出力しなさい、と指定しています。作成されたこのファイルを開くと、以下のようになっています。

Turbo Dump  Version 5.0.16.12 Copyright (c) 1988, 2000 Inprise Corporation
                    Display of File ENUMWND.OBJ

000000 THEADR  enumwnd.c
000D9C LNAMES
    Name  1: '_TEXT'
    Name  2: 'CODE'
    Name  3: ''
    Name  4: '_DATA'
    Name  5: 'DATA'
    Name  6: 'DGROUP'
    Name  7: '_BSS'
    Name  8: 'BSS'
000DC7 SEGDEF 1 : _TEXT     DWORD PUBLIC  USE32 Class 'CODE'   Length: 002f
000DD1 SEGDEF 2 : _DATA     DWORD PUBLIC  USE32 Class 'DATA'   Length: 0000
000DDB SEGDEF 3 : _BSS      DWORD PUBLIC  USE32 Class 'BSS'    Length: 0000
000DE5 GRPDEF Group: DGROUP
    Segment: _BSS           
    Segment: _DATA          
000E23 LEDATA  Segment: _TEXT          Offset: 0000  Length: 002F
    0000: 55 B8 22 22 22 22 8B EC  53 8B 18 B9 33 33 33 33   Uク""""駆S..ケ3333
    0010: 3B CB BA 11 11 11 11 7F  04 33 C0 EB 0D 8B 4D 08   ;ヒコ......3タ..貴.
    0020: 89 0C 9A FF 00 B8 01 00  00 00 5B 5D C2 08 00      .....ク....[]ツ..
000E59 MODE32

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


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

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

    0000: 55 B8 22 22 22 22 8B EC  53 8B 18 B9 33 33 33 33

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

55 B8 22 22 / 22 22 8B EC / 53 8B 18 B9 / 33 33 33 33 / …

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

fncode.0 = $2222B855
fncode.1 = $EC8B2222
fncode.2 = $B9188B53
fncode.3 = $33333333

1つ注意しておくことは、配列変数の要素数です。上の部分で、『Length: 002F』と表示されているのが分かると思いますが、これは、コード全体のサイズが16進数で $2F バイト(10進数では 47 バイト)であることを示しています。したがって、これだけのサイズを格納できるように配列変数を確保しておかなければなりません。今回の場合は配列の要素数は 12 になります。(今回の場合は 16 以下なのであえて確保する必要はありませんが。)

スクリプトで使う

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

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

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

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

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

    ; マシン語を変数に格納する (必ず xdim を使用すること)
    xdim fncode, 12
    fncode.0 = $2222b855, $ec8b2222, $b9188b53, $33333333, $11bacb3b, $7f111111
    fncode.6 = $ebc03304, $084d8b0d, $ff9a0c89, $0001b800, $5d5b0000, $000008c2

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

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

    ; 列挙するウィンドウの最大数(配列の要素数)を$Cバイト目にセット
    t = 512
    memcpy fncode, t, 4, $C

    ; ウィンドウハンドルを格納する配列変数のアドレスを$13バイト目にセット
    dim hwnd, 512
    getptr t, hwnd
    memcpy fncode, t, 4, $13

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

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

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