まずは、 EnumWindows 関数を使用してウィンドウハンドルを列挙する場合に使用される、 EnumWindowsProc コールバック関数のコードを作成してみます。
HSP スクリプト中で呼び出す EnumWindows 関数は以下のように定義されています。
BOOL EnumWindows( WNDENUMPROC lpEnumFunc, // コールバック関数のアドレス LPARAM lParam // アプリケーション定義値 );
この関数の lpEnumFunc パラメータに指定するものは、アプリケーション側で実装された EnumWindowsProc コールバック関数のアドレスになります。
BOOL CALLBACK EnumWindowsProc( HWND hwnd, // ウィンドウハンドル LPARAM lParam // アプリケーション定義値 );
アプリケーションが EnumWindows 関数を呼び出したとき、存在するトップレベルウィンドウの数だけ EnumWindowsProc コールバックが呼び出されるようになっています。このとき、コールバック関数の hwnd パラメータに、それぞれのトップレベルウィンドウのハンドルが渡されています。また、今回は使用しませんが、 EnumWindowsProc の lParam パラメータには、 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 側の変数であるとし、プログラム中ではその変数を指すポインタを使用します。ソースプログラムは以下のようになります。
#include <windows.h> BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam) { HWND *volatile p_hwnd = (HWND *)// 配列変数アドレス int *volatile p_cnt = (int *) ; // カウンタ変数アドレス volatile int max_num = ; // 最大数 if (*p_cnt >= max_num) return FALSE; p_hwnd[*p_cnt] = hwnd; (*p_cnt)++; return TRUE; };
ウィンドウハンドル用配列変数やカウンタ用変数のアドレスは、実際にはスクリプト実行時まで分かりません。従って、とりあえずは上のように
などのような、後で見て一目でわかる数値をキャストして入れておきます。ここでは、取得できる最大数も変えられるように、上のようにしておきます。ここで、後から値を埋め込む変数やポインタの宣言時には、最適化の防止を行なう修飾子 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 の場合は、
という順序で設定できます。
ファイルをコンパイルすると、設定されているディレクトリ(デフォルトは「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 mov eax, 572662306 ; 22222222H ; Line 9 00005 8b 08 mov ecx, DWORD PTR [eax] 00007 81 f9 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 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進数表示になっていることに注意してください。スクリプトを実行した時にマシン語コードがメモリ上にこのように配置されるように、配列変数に値を格納しなければなりません。
今回はコードの量が少ないですし、今回が初めてであるということもあるので、配列変数に直接書き込んでいくことにします。ただし、上の「」などの部分は、後でスクリプト上で別の値(変数アドレスなど)を格納するためのものなので、この部分は 0 などでもかまいませんが。
さて、 Windows のメモリ管理は「リトルエンディアン方式」と呼ばれ、下位バイトのほうが先に格納されるようになっています。すなわち、32ビット数値 0x12345678 は、メモリ上で「78 56 34 12」の順で格納されるのです。従って、
00000 b8 00005 8b 08 00007 81 f9 0000d 7c 05 0000f 33 c0
となっている部分のマシン語コードを配列変数に格納するには、まず、以下のように並べて、これを4バイトずつに区切ってみます。
b8 / / 8b 08 81f9 / / c0 … 7c 05 33
それをバイトごとに逆転させて格納します。
fncode.0 = $b8 fncode.1 = $81088b fncode.2 = $ f9 fncode.3 = $33057c …
これでマシン語がメモリ上に格納できたわけです。 EnumWindows 関数呼び出し時に、コールバック関数のアドレスが必要になりますが、これはこのマシン語の先頭のアドレス、すなわち、変数 fncode のアドレスを指定すればいいのです。
さて、これで、そのまま EnumWindows 関数を呼び出してウィンドウハンドルが取得できるかというと、そうではありません。上のマシン語コード上で「」などとなっていた部分に正しい値を格納しなければなりません。それぞれの数値のオフセットを考えて、 memcpy や ll_poke4 などで正しい値を格納しましょう。上のコードの場合は、以下のようになります。
オフセット | 仮の値 | 実際の値 |
---|---|---|
1 | カウンタ変数のアドレス | |
9 | 列挙する最大数(=配列変数の要素数) | |
$1b (27) | ハンドルを格納する配列変数のアドレス |
以上から、全体のスクリプトは次のようになります。
#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