◆フック & DLL - VC++ 2005

[ ホーム ] [ 上へ ] [ フック & DLL - VC++ 2005 その2 ]

・概要

表題は「フック & 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)
フックには別処理したいメッセージの種類や処理するタイミングなどによっていくつかに 分類されています。これはフックタイプと呼ばれていて以下のものがあります。

タイプ名 説明
WH_CALLWNDPROC メッセージがウィンドウプロシージャに送られる直前にシステムが フックプロシージャ(メッセージ処理/モニタを可能にする関数)をコールします。
WH_CALLWNDPROCRET メッセージがウィンドウプロシージャに送られたにシステムが フックプロシージャをコールします。メッセージがウィンドウプロシージャに送られた後なので メッセージの変更はできません。
WH_CBT ウィンドウが作られたり、破棄されたりするときにシステムが フックプロシージャをコールします。
WH_DEBUG 他のフックプロシージャが呼ばれる前にコールされます。つまり、 他のフックプロシージャ自体を監視します。
WH_FOREGROUNDIDLE フォアグラウンドのスレッドがアイドルになるときにシステムがフックプロシージャを コールします。
WH_GETMESSAGE メッセージがGetMessage、PeekMessageでメッセージキューから取得されるときに システムがフックプロシージャをコールします。
WH_JOURNALPLAYBACK フックタイプがJournalRecordのフックプロシージャでマウス・キーボードの メッセージが記録されて、それを再生するためにこのタイプのフックプロシージャを使用します。 マウス・キーボード入力は無効になります。
WH_JOURNALRECORD キューから削除されたメッセージを記録して、WH_JOURNALPLAYBACKタイプの フックプロシージャで再生します。
WH_KEYBOARD メッセージがGetMessage、PeekMessageでメッセージキューから取得されるときに そのメッセージがキーボード関連(WM_KEYUPなど)の場合にシステムがフックプロシージャを コールします。
WH_KEYBOARD_LL システムがメッセージキューにメッセージをポストするときに呼び出される、 低水準のキーボード入力フックプロシージャです。
WH_MOUSE メッセージがGetMessage、PeekMessageでメッセージキューから取得されるときに そのメッセージがマウス関連(WM_LBUTTONUPなど)の場合にシステムがフックプロシージャを コールします。
WH_MOUSE_LL システムがメッセージキューにメッセージをポストするときに呼び出される、 低水準のマウス入力フックプロシージャです。
WH_MSGFILTER ダイアログボックス、メッセージボックス、メニュー、スクロールバーに 関するメッセージが発生したときにシステムがフックプロシージャをコールします。
WH_SHELL シェル関連のメッセージ(ウィンドウの最小化/最大化など)が発生したときに システムがフックプロシージャをコールします。
WH_SYSMSGFILTER ダイアログボックス、メッセージボックス、メニュー、スクロールバーに 関するメッセージが発生したときにシステムがフックプロシージャをコールします。

フックはシステム全体の処理を重くしたり、デスクトップ全体に影響するため、 すべてのメッセージを際限なくフックするのではなく、上記のうち目的のフックタイプを選択して 影響を出来るだけ小さくしようとしています。

・アプリケーションのインスタンスハンドル(第三引数 - 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_QS00
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の変数の共有化について詳しく書いたので ご覧ください。