・概要
表題は「フック & DLL」となっていますが、主はフックについてで、DLLに関しては フックを利用するコードは多くの場合DLLに格納しなければならず、おのずとDLLのことをある程度は 知っておかないといけないということからこの表題としました。ということで、フックの解説を していく中で必要に応じてDLLのことを書いていきます。
<Windowsアプリケーション>
フックとは「既定のメッセージ処理のメカニズムの変更/追加/モニタリング」と 理解しています。既定のメッセージ処理って?ということになりますが、これは ウィンドウズアプリケーションの根幹部分の話で、マルチスレッド で一応のことは書いているのですがもう一度さらっと書きます。
ウィンドウズアプリケーションの動作の概要を書いた図をご覧ください。
1.exeファイルを実行するとプロセスが作られWindowsからメモリが割り当てられる 2.プロセス内にスレッドが構築されメッセージキューなどが作られ、スレッド制御関数 (WinMain)が実行される。 3.スレッド制御関数でウィンドウが作られたのち(ウィンドウプロシージャを指定して作られる)、 メッセージポンプが開始されてこの制御関数は半無限ループになる。 4.OS(Windows)からWindowメッセージがスレッドキューに送られてくるのをメッセージポンプ が適切なウィンドウ(ウィンドウプロシージャ)へDispatch(分岐)する。 5.ウィンドウプロシージャに送られたWindowメッセージはプログラマによって記述された処理を 実行する。
つまり、プログラミング可能なアプリケーションを特徴づける動作の指定 (Windowメッセージの処理)は、特定のウィンドウへのメッセージではないメッセージなどの例外は あるものの、多くの場合はウィンドウプロシージャ内に記述することだけです。 それは、exe内部からしか制御/監視はできません。
このメッセージ処理のメカニズムに追加の処理機能を与えようというのが フックの考えです。
<SetWindowsHookEx>
フックはSetWindowsHookExというAPIで実行可能になりますが、その定義は 次のとおりです。
HHOOK SetWindowsHookEx( int idHook, // フックタイプ HOOKPROC lpfn, // フックプロシージャ HINSTANCE hMod, // アプリケーションインスタンスのハンドル DWORD dwThreadId // スレッドの識別子 );
・フックプロシージャ(第二引数 - lpfn)
上記の通常のメッセージ処理機構とは別のメッセージ処理プロシージャ。上図のようにこのプロシージャは
当該スレッドの外部に記述することができ(DLLに配置)、通常のメッセージ処理を変更することも可能です。
・フックタイプ(第一引数 - idHook)
フックには別処理したいメッセージの種類や処理するタイミングなどによっていくつかに
分類されています。これはフックタイプと呼ばれていて以下のものがあります。
フックはシステム全体の処理を重くしたり、デスクトップ全体に影響するため、 すべてのメッセージを際限なくフックするのではなく、上記のうち目的のフックタイプを選択して 影響を出来るだけ小さくしようとしています。
・アプリケーションのインスタンスハンドル(第三引数 - hMod)
フックプロシージャがあるモジュール(DLL)のハンドル。上図ではc.dllの
インスタンスハンドル。実際の取得はDLLの場合はそのエントリポイントDllMain関数の第一引数で
指定されているhModuleが該当するので、この値を保持しておきます。
この値は次の第四引数(dwThreadId)とも関係があり、上図で説明すると、フックしようとしている
スレッドがDスレッド(dwThreadIdがDスレッドのスレッドID)で、さらにフックプロシージャが
c.dllではなくDスレッドにある場合、つまり自身のスレッドを
フックする場合はDスレッドのインスタンスハンドル(B.exe)ではなくてNULLを指定します。
ただ、自身のスレッドをフックするのはあまり用途がありません。フックを使わなくても
通常のメッセージポンプ→フックプロシージャで処理すればいい場合が多いからです。
・スレッドの識別子(第四引数 - dwThreadId)
フックを実行したいスレッドの識別子(スレッドID)を指定します。
上図ではDスレッドのスレッドIDです。フックするスレッドはある特定のスレッドでもいいですし、
0でもいいです。ここに0を指定するとデスクトップ上の全スレッドが対象になります。
前者はローカルフック, 後者はグローバルフックと呼ばれています。グローバルフック
はシステム全体に影響を及ぼすため、というかシステムクラッシュしますので、
後で例を掲載しますが十分注意してあくまで自己責任に試してください。
SetWindowsHookExの各引数を上記の内容で実行すると、Windowsは第一引数に指定した
フックタイプごとにあるフックチェーンに、第三引数に指定したモジュールへの
ハンドルから第二引数のフックプロシージャへのポインタを加えます(フックチェーンへのインストール)。
フックチェーンはフックプロシージャへのポインタのリスト構造になっていて、
この先頭位置にポインタが追加されます。(以前に他のプロセスがSetWindowsHookExを実行していたら、
インストールされたポインタの位置以下にそれらのフックプロシージャへのポインタが格納されています)
フックチェーンにフックプロシージャへのポインタがインストールされたら、 あるフックタイプのメッセージが発生すると、システムは該当のフックタイプの フックチェーンの先頭にあるフックプロシージャを呼び出します。
フックプロシージャ内で任意のメッセージ処理を行った後は、フックチェーンの後続の フックプロシージャを最後まで呼んでいかなければなりません。これは、CallNextHookExを フックプロシージャ内部からコールすることで解決します。MSDNにも書かれていますが、多くの 場合、CallNextHookExをコールしてその戻値をフックプロシージャの戻値とするように強く 推奨されています。もし、CallNextHookExがコールされないと、フックチェーンの後につづく フックプロシージャが呼ばれなくなって、それらのフックプロシージャをインストールした プロセスは異常を起こしてしまいます。
フックを終了させるのはSetWindowsHookExの戻値で得たフックハンドルを 引数にしてUnhookWindowsHookExをコールするだけです。
<フックプロシージャ>
フックしたメッセージ対する処理は第二引数に指定したフックプロシージャで 行いますが、そのプロトタイプは
LRESULT CALLBACK HookProc(int nCode, WPARAM wParam, LPARAM lParam);
です。HookProcはプレースホルダなので実際にコーディングする場合は任意の関数名 で構いません。フックプロシージャはシステムによって呼び出されることは <フックの仕組み>で書いたとおりですが、呼ばれたときに引数に 収められている各値はフックタイプごとに異なっています。
フックタイプ | 引 数 | ||
nCode | wParam | lParam | |
WH_CALLWNDPROC | フックプロシージャのメッセージ処理の必要性の有無 (負の時:CallNextHookExを呼ぶ HC_ACTIONの時: メッセージの処理が必要) |
メッセージが現在のプロセスから送られたときは0以外 それ以外は0 |
構造体CWPSTRUCTへのポインタ
typedef struct { LPARAM lParam; WPARAM wParam; UINT message; HWND hwnd; } CWPSTRUCT; |
WH_CALLWNDPROCRET | 同上 | 同上 | 構造体CWPRETSTRUCTへのポインタ
typedef struct { LRESULT lResult; LPARAM lParam; WPARAM wParam; UINT message; HWND hwnd; } CWPRETSTRUCT; |
WH_CBT | HCBT_ACTIVATE | アクティブ化しようとしているウィンドウハンドル | 構造体CBTACTIVATESTRUCTへのポインタ
typedef struct { BOOL fMouse; HWND hWndActive; } CBTACTIVATESTRUCT; |
HCBT_CLICKSKIPPED | メッセージキューから削除されたマウスメッセージ | 構造体MOUSEHOOKSTRUCT構造体へのポインタ。マウスフックがインストールされている
時に限り有効
typedef struct { POINT pt; HWND hwnd; UINT wHitTestCode; ULONG_PTR dwExtraInfo; } MOUSEHOOKSTRUCT |
|
HCBT_CREATEWND | 新しいウィンドウのハンドル | 構造体CBT_CREATEWNDへのポインタ
typedef struct { LPCREATESTRUCT lpcs; HWND hwndInsertAfter; } CBT_CREATEWND; | |
HCBT_DESTROYWND | 破棄されようとしているウィンドウハンドル | 0 | |
HCBT_KEYSKIPPED | 仮想キーコード | キーリピート回数、スキャンコード、キー遷移コード、キー直前の状態、コンテキストコード キーボードフックがインストールされている時に限り有効 | |
HCBT_MINMAX | 最小化または最大化されようとしているウィンドウハンドル | 下位ワード(下位16bit)でウィンドウの表示状態(ShowWindowの引数)の値 | |
HCBT_MOVESIZE | 移動またはサイズ変更しようとしているウィンドウハンドル | ウィンドウの座標を保持しているRECT構造体へのポインタ | |
HCBT_QS | 0 | 0 | |
HCBT_SETFOCUS | キーボードフォーカスを受け取るウィンドウハンドル | キーボードフォーカスを失うウィンドウハンドル | |
HCBT_SYSCOMMAND | システムコマンドの値(SC_の値) | WM_SYSCOMMANDのlParamと同じ値 | |
負の値の時はCallNextHookExをコール | − | − | |
WH_DEBUG | フックプロシージャのメッセージ処理の必要性の有無 (負の時:CallNextHookExを呼ぶ HC_ACTIONの時: メッセージの処理が必要) |
WH_CALLWNDPROC | 構造体DEBUGHOOKINFOへのポインタ
typedef struct { DWORD idThread; DWORD idThreadInstaller; LPARAM lParam; WPARAM wParam; int code; } DEBUGHOOKINFO; |
WH_CALLWNDPROCRET | |||
WH_CBT | |||
WH_DEBUG | |||
WH_GETMESSAGE | |||
WH_JOURNALPLAYBACK | |||
WH_JOURNALRECORD | |||
WH_KEYBOARD | |||
WH_MOUSE | |||
WH_MSGFILTER | |||
WH_SHELL | |||
WH_SYSMSGFILTER | |||
WH_FOREGROUNDIDLE | 同上 | 未使用 | 未使用 |
WH_GETMESSAGE | 同上 | PM_NOREMOVE (メッセージがキューから削除されていない状態。PM_NOREMOVEを指定してPeekMessageが コールされている) |
構造体MSGへのポインタ
typedef struct { HWND hwnd; UINT message; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt; } MSG, *PMSG; |
PM_REMOVE (メッセージがキューから削除されている状態。GetMessage、またはPM_REMOVEでPeekMessageが コールされている) | |||
WH_JOURNALPLAYBACK | HC_GETNEXT | 未使用 | 構造体EVENTMSGへのポインタ
(nCodeがHC_GETNEXTの時のみ有効)
typedef struct { UINT message; UINT paramL; UINT paramH; DWORD time; HWND hwnd; } EVENTMSG; |
HC_NOREMOVE | |||
HC_SKIP | |||
HC_SYSMODALOFF | |||
HC_SYSMODALON | |||
負の値の時はCallNextHookExをコール | |||
WH_JOURNALRECORD | HC_ACTION | 未使用 | 構造体EVENTMSGへのポインタ
typedef struct { UINT message; UINT paramL; UINT paramH; DWORD time; HWND hwnd; } EVENTMSG; |
HC_SYSMODALOFF | |||
HC_SYSMODALON | |||
負の値の時はCallNextHookExをコール | |||
WH_KEYBOARD | HC_ACTION | キーストロークメッセージを生成したキーに対応する仮想キーコード | キーリピート回数、スキャンコード、拡張キーフラグ、 コンテキストコード、キーの直前の状態、遷移状態 |
HC_NOREMOVE | |||
負の値の時はCallNextHookExをコール | |||
WH_KEYBOARD_LL | HC_ACTIONの時:wParam,lParamにキーボードメッセージに関する情報を保持 負の値の時:CallNextHookExを呼ぶ |
WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, WM_SYSKEYUP のいずれか |
構造体KBDLLHOOKSTRUCTへのポインタ
typedef struct { DWORD vkCode; DWORD scanCode; DWORD flags; DWORD time; ULONG_PTR dwExtraInfo; } KBDLLHOOKSTRUCT; |
WH_MOUSE | HC_ACTION | マウスメッセージの識別子 | 構造体MOUSEHOOKSTRUCTへのポインタ
typedef struct { POINT pt; HWND hwnd; UINT wHitTestCode; ULONG_PTR dwExtraInfo; } MOUSEHOOKSTRUCT; |
HC_NOREMOVE | |||
負の値の時はCallNextHookExをコール | |||
WH_MOUSE_LL | HC_ACTIONの時:wParam,lParamにキーボードメッセージに関する情報を保持 負の値の時:CallNextHookExを呼ぶ | WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_RBUTTONDOWN
WM_RBUTTONUP のいずれか |
構造体MSLLHOOKSTRUCTへのポインタ
typedef struct { POINT pt; DWORD mouseData; DWORD flags; DWORD time; ULONG_PTR dwExtraInfo; } MSLLHOOKSTRUCT; |
WH_MSGFILTER | MSGF_DDEMGR | 未使用 | 構造体MSGへのポインタ
typedef struct { HWND hwnd; UINT message; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt; } MSG; |
MSGF_DIALOGBOX | |||
MSGF_MENU | |||
MSGF_SCROLLBAR | |||
負の値の時はCallNextHookExをコール | |||
WH_SHELL | HSHELL_ACCESSIBILITYSTATE | ACCESS_FILTERKEYS, ACCESS_MOUSEKEYS, ACCESS_STICKYKEYS のいずれか |
− |
HSHELL_ACTIVATESHELLWINDOW | − | − | |
HSHELL_APPCOMMAND | WM_APPCOMMANDメッセージが最初に発行された場所 (WM_APPCOMMANDのcmdパラメータ) |
GET_APPCOMMAND_LPARAM(lparam)マクロ,GET_DEVICE_LPARAM(lparam)マクロ, GET_FLAGS_LPARAM(lpara)マクロで使用 | |
HSHELL_GETMINRECT | 最小化または最大化対象のウィンドウハンドル | 構造体RECTへのポインタ
typedef struct _RECT { LONG left; LONG top; LONG right; LONG bottom; } RECT; | |
HSHELL_LANGUAGE | ウィンドウハンドル | キーボードレイアウトのハンドル | |
HSHELL_REDRAW | 再描画されたウィンドウハンドル | ウィンドウが点滅している時:TRUE それ以外:FALSE | |
HSHELL_TASKMAN | − | − | |
HSHELL_WINDOWACTIVATED | アクティブ化されたウィンドウハンドル | ウィンドウがフルスクリーンモードの時:TRUE それ以外:FALSE | |
HSHELL_WINDOWCREATED | 作成されたウィンドウハンドル | − | |
HSHELL_WINDOWDESTROYED | 破棄対象のウィンドウハンドル | − | |
負の値の時:CallNextHookExをコール | − | − | |
WH_SYSMSGFILTER | MSGF_DIALOGBOX | 未使用 | 構造体MSGへのポインタ
typedef struct { HWND hwnd; UINT message; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt; } MSG; |
MSGF_MENU | |||
MSGF_SCROLLBAR | |||
負の値の時:CallNextHookExをコール |
戻値に関してはWH_CBTやWH_JOURNALxxxタイプのフックで例外はあるものの CallNextHookExのコールによって得られる値が強制または強い推奨ということになっています。 つまり、ほとんどの場合でフックプロシージャ内でCallNextHookExをコールしなければなりません。
実際にフックプロシージャを記述する場所ですが、 自身のスレッドをフックするローカルフックの場合は、exe内に記述してSetWindowsHookExの 必要な引数で、フックプロシージャが含まれるモジュールのインスタンスハンドルはNULL, フック対象のスレッドIDはGetCurrentThreadIdの戻値をあてれば簡単にできます。
しかし、このようなフックは稀で、たいていはSetWindowsHookExの第四引数の
dwThreadIdを0にしてすべてのスレッドをフックする
グローバルフックか、
フックの動作の模式図のように、A.exeがB.exeのメッセージ状態を
監視するような他のプロセスのスレッドのメッセージを監視するタイプのグローバルフックです。
ということは、フックプロシージャは自身のプロセス以外のコンテキストでも呼ばれることになるので
DLLにする必要があります(逆にexeにフックプロシージャが書かれていたら、フ
ックしようとしているスレッドのあるプロセス空間でフックプロシージャが実行できません)。
全スレッドを対象とした
グローバルフックの場合、
フックの動作の模式図では対象がB.exeだけですが、
実際にはC.exe, D.exe・・・と多くのプロセスでフックプロシージャがコールされます。
複数のプロセスで実行されるプロシージャ内で、ほとんどの場合でCallNextHookExを
コールしなければならないことを考えると、CallNextHookExで使うフックハンドル
(SetWindowsHookExでフックのセットが成功したときに戻されるハンドル)が有効なのか
ということも考慮しなければなりません。
実は、DLLのグローバル変数(フックハンドルはグローバル変数になる)は 通常では各プロセスごとに新しいインスタンスができてしまい、各プロセスでフックハンドルが 有効でなくなる可能性があります。(DLLの読込み模式図参照) これを解決するために変数の共有化をして全スレッドで一意にしないといけません。
これはフックというよりはDLLの範囲なので、フックに必要な知識として DLLのリンク方法、 エクスポート方法(関数、クラスをDLL外部に見せる方法) を含めて DLLの変数の共有化について詳しく書いたので ご覧ください。