・DLL
フックの仕組み/流れが分かったところで実際にコーディングをするわけですが、
システムに大規模な影響を与える可能性がある作業にもかかわらず、やることは思ったほど
たいへんではないです。
基本はフックプロシージャを作成してSetWindowsHookExをコールするだけです。ローカルフック
を除いて多くの場合はDLLにフックプロシージャを記述することになります。そこでここでは、
フックに関連するDLLについての基本事項を確認してみたいと思います。
<リンク>
まずは、DLLを利用する側(exe側)から見てみます。 DLLは単独で実行できません。主に実行形式(exe)にリンクされて使われます。つまり、 exe内でDLL内の関数を使用しなければならないとき、DLLがロードされてexeのプロセス空間に読み込まれ ます。ただ、exeが自身のプロセス空間に読み込む方法・タイミングについて2種類のリンク方法が あり、それぞれ暗黙的リンク、明示的リンクです。
・暗黙的リンク(静的)
DLLをビルドするといくつかファイルが作成されますが、公開してexe側で使ってもらうために
配布すべきファイルが3つあります。
DLL内の関数(クラス)を使ってあるexeを開発しようとしているとき、まず、
最初のDLLのヘッダファイルをインクルードします。これは当然理解できると思います。
このヘッダに記述されている関数をexeの実装ファイル(cpp)で利用したとしましょう。
これでexeをビルドした場合、コンパイラはexeの実装ファイルからオブジェクトファイルを生成した後、
リンカはヘッダに書かれた関数を作成されたオブジェクトから探しだしてリンクします。
ところが、DLLにある関数についてはリンカは関知しないので「そんな関数はないよ」とエラーを吐きます。
そこで登場するのが2番目のライブラリファイルです。ライブラリファイル内には関数の実体はなく、
DLLのロードとその中の関数の呼び出しを実行するコードがあるだけです。
このファイルの中を#pragmaディレクティブを使って明示的にリンカに探させます。
リンカがライブラリファイル内に目的の関数を見つければリンクが成功します。
ここで注意したいのはDLL本体の中身を検索しているのではなくライブラリファイル内を検索しているという
ことです。つまり、exeのビルド時にリンクしているのはDLL本体ではなくライブラリファイル(lib)
ということです。
最後のDLL本体はいつ使われるの?ということになりますが、実際にexeを実行する時点で DLL本体を読み込みます。このときに適切な位置(システムフォルダなど)にDLL本体がなければエラーに なってexeは実行に失敗します。このような方法でのDLLとのリンクを暗黙的リンク(静的リンク) と呼んでいます。
DLL側 | EXE側 |
<出力ファイル名> hoge.dll (本体) hoge.lib (ライブラリファイル) hoge.h (ヘッダファイル) |
//-------.cpp---------------- #include "hoge.h" #pragma comment(lib, "hoge.lib") ・ ・ int n = fnHoge(); ・ ・ |
//-----hoge.h------------- __declspec(dllexport) int fnHoge(); //-----hoge.cpp----------- int fnHoge(){ ・ ・ } //(__declspec(dllexport)は後で解説するので この時点では無視してください) |
DLLの暗黙的リンクのやり方は上表のようですが、暗黙的リンクの特徴は exeのコンパイル時にどのDLLを使うか決定していてそれ以外ではビルドできないということです。 これはある意味柔軟性にかけるリンクの方法ですが、簡単にリンクできるのと、DLLに関数ではなくクラスが あるとこちらのほうが便利です。
・明示的リンク(動的)
暗黙的リンクは手軽にDLLを利用できますが、例えば、exeの実行状況によって読み込みたいDLLを変化させたい
場合などは、exe実行時にDLLを読み込んでしまうのでちょっと困ります。そこで、exe実行時にはDLLは
読み込まず、任意の時期にプログラマが明示的にDLLをロードを指示するタイプのリンクが
明示的リンクです。
このタイプのリンクは自由にDLLをロード/アンロードでき、ライブラリファイル
(lib)を使用しないで、GetProcAddressというAPIでDLL内の関数へのポインタを取得することによって
DLLを利用します。なので、DLLのクラスを直接利用しようと困ります。(実は後で書きますが、
暗黙的リンクを使ってもDLLのクラスを直接利用するのはあまり好ましくないのでこの点は
あまり気にする必要がないかもしれません。)
もう一点は既にDLLがロードされていて、明示的リンクする(LoadLibraryを実行)とDLLのエントリポイント
の関数であるDllMainが呼ばれないという点です。
DLL側 | EXE側 |
<出力ファイル名> hoge.dll (本体) hoge.h (ヘッダファイル) |
//-------.cpp---------------- #include "hoge.h" ・ ・ typedef int (CALLBACK *PFNHOGE)(); HINSTANCE hDll = LoadLibrary("hoge.dll"); PFNHOGE pfnHoge = (PFNHOGE)GetProcAddress( hDll, "fnHoge"); int n = pfnHoge(); ・ ・ FreeLibrary(hDll); |
//-----hoge.h------------- extern "C" __declspec(dllexport) int fnHoge(); //-----hoge.cpp----------- extern "C" int fnHoge(){ ・ ・ } //(extern "C" __declspec(dllexport)は後で 解説するのでこの時点では無視してください) |
上表のようにLoadLibraryでDLLを読み込んでハンドルを取得します。そのハンドルを
使ってDLLに実装された関数(fnHoge)へのポインタをGetProcAddressで取得することによって
DLLを利用します。プログラム内でDLLのインスタンスハンドルからDLL内の関数へのポインタを
直接取得するのでライブラリファイルを必要としていません。
暗黙的リンクの場合はライブラリファイル(lib)にexeが利用してもよい関数が含まれていなければ
ならないし、明示的リンクの場合はGetProcAddressで関数へのポインタが取得されなければなりません。
これがうまくいくようにするには、DLL内の関数(クラス)をexeの時のように実装するだけでは不足です。
<エクスポート>
exeはそれ自体が実行可能ファイルで、通常は外部からexe内部の関数やクラスを 直接利用できたりはしません。そんなことしたらプログラム外から予期せぬ制御が入り込み たいへんなことになってしまいます。一方、DLLは逆に内部の関数(クラス)を外部から使ってもらう ために作成するので、その内容(関数名など)を外部に見せないといけません。これを エクスポートといいます。
エクスポートを行うとDLL自体だけでなく、ライブラリファイルにもDLL内の 関数名やその呼び出しコードなどが書き込まれます。exe側で 暗黙的リンクを使う場合は、 エクスポート処理でライブラリファイルに書き込まれた情報を、 明示的リンクを使う場合は、 エクスポート処理でDLL本体に書き込まれた情報を利用することになります。 逆にエクスポートされなかった関数(クラス)は外部から直接利用できません。
エクスポートの方法もリンクと同様いくつかの方法があり、MSの推奨順に以下のとおりです。
(1.__declspec(dllexport)でのエクスポート)
1.は関数にもクラスにも適用できてたいへん便利です。実際、ウィザードで
Win32 DLLプロジェクトを作成する際に、シンボルのエクスポートオプションにチェックすると
__declspec(dllexport)でエクスポートするスケルトンが作成されます。(VC++ 2005)
使用方法(基本) | |
エクスポートの種類 | エクスポート方法 |
データ | __declspec(dllexport) int n; |
関数 | __declspec(dllexport) int fnHoge(); //(ヘッダファイルの宣言時) |
クラス | class __declspec(dllexport) CHoge { }; |
クラス(一部のみエクスポート) |
class CHoge
{
__declspec(dllexport) CHoge();
__declspec(dllexport) ~CHoge();
__declspec(dllexport) int func1();
int func2(); //func2はエクスポートしない
}; |
基本は上記のとおりですが、実際はこのように直接__declspec(dllexport)を記述することは
ほとんどありません。これが第一の注意点ですが、直接記述するとDLL内の関数(クラス)を利用した
exe側からもDLL内の関数(クラス)がエクスポート可能になってしまいます。
例えば下記のようにDLLに関数が宣言されて、その関数がエクスポートされたとします。
// hoge.h - DLL側 __declspec(dllexport) int fnHoge(); // hoge.cpp - DLL側 int fnHoge() { return 0; }
このDLLをexe側でインクルードします。
// hoge_exe.h - EXE側
#include "hoge.h"
//プリプロセッサがこの位置に
__declspec(dllexport) int fnHoge();を書き込む
つまり、exe側のfnHogeがエクスポートされてしまいます。exeは 前述したとおり内部の関数は外部にみせることはしないし、 してはいけないです。この二重エクスポートを避けるために下記のようにエクスポートスイッチ を使うようにします。
DLL側 | EXE側 |
// ---------- dll_define.h --------------- #define EXEC_EXPORT // ---------- export_switch.h ------------ #ifdef EXEC_EXPORT #define DLL_API __declspec(dllexport) #else #define DLL_API __declspec(dllimport) // ----------------- hoge.h -------------- DLL_API int fnHoge(); class DLL_API CHoge { }; class CHogeHoge { DLL_API CHogeHoge(); DLL_API ~CHogeHoge(); DLL_API int func1(); int func2(); }; // ---------------- hoge.cpp ---------------- #include "dll_define.h" #include "export_switch.h" #include "hoge.h" //・ // (dll_define.hがインクルードされるので // DLL_APIは__declspec(dllexport)ととして処理) //・ |
// ------ hoge_exe.cpp (暗黙的リンク) ----
#include "export_switch.h"
#include "hoge.h"
#pragma comment(lib, "hoge.lib")
//・
// (dll_define.hがないのでDLL_APIは
// __declspec(dllimport)ととして処理)
//・
|
上記のように書けばDLL側ではプリプロセッサはDLL_APIを__declspec(dllexport)として、 EXE側ではDLL_APIを__declspec(dllimport)として処理します。__declspec(dllimport)は __declspec(dllexport)とは反対に、これが記述された関数・クラスはDLLからインポートするという 意味になります。これによりEXE側から外部へのfnHoge関数・CHoge, CHogeHogeクラスのエクスポートは 防げます。
ただ、この__declspec(dllimport), __declspec(dllexport)の切り替えに関しては そんなに気にしなくてもいいです。というのはWin32プロジェクトでDLLプロジェクトを 作るときアプリケーションウィザードを起動して、追加のオプションでシンボルのエクスポート チェックボックスをONにするとスケルトンを自動的に書き加えてくれます。
上記とちょっと違うのはdll_define.hに相当するヘッダファイルはなく、 コンパイルのプリプロセッサのオプションとして事前に定義されているということです。 では確認してみます。ツールバーのファイル(F)→新規作成(N)→プロジェクト(P)で アプリケーションウィザードを起動して以下のように設定してください。(VC++ 2005)
プロジェクトの種類 : Win32
テンプレート : Win32プロジェクト
プロジェクト名 : (任意) →OK
→次へ
(アプリケーションの設定)
アプリケーションの種類 : DLL
追加のオプション : シンボルのエクスポートにチェック
→完了
ウィザードが作成してヘッダ・実装ファイルの中身に上記のような __declspec(dllexport), __declspec(dllimport)スイッチがあることを確認した後、 ツールバーのプロジェクト(P)→xxxxのプロパティ(P)でプロパティページを表示してください。
構成 : 構成プロパティ
- C/C++
- プリプロセッサ
プリプロセッサの定義 : WIN32;_DEBUG;・・・;xxxx_EXPORTS
上記の箇所にdll_define.hのEXEC_EXPORTに相当するシンボルが 定義されていることを確認してください。
もうひとつの注意点は、関数のエクスポート時の装飾名についてです。 EXE側が明示的リンクをしていて、DLLの関数を使おうとするときに起きます。 明示的リンクの場合はDLL側の関数名を指定してGetProcAddressを呼び出します。例えば、 DLL側で
declspec(dllexport) int funcHoge(int n);
という関数を宣言・定義したとします。EXE側で
typedef int (*PFNHOGE)(int); PFNHOGE pfnHoge = (PFNHOGE)GetProcAddress(hMod, "funcHoge");
とすれば問題ないように思えますが、pfnHogeの値はNULLになりうまくいきません。
GetProcAddressに指定した関数名が問題です。そこでまず、DLL側でどのようにエクスポート
されているかみてみます。
それを見るツールがdumpbin.exeです。使い方は
DUMPBINレファレンスにあるので確認してください。デフォルトで
C:\Program Files\Microsoft Visual Studio 8\VC\bin (Visual C++ 2005 Standard Editionの場合)
にあると思うので、ライブラリファイル内のエクスポートされた関数をみてみると
C:\>dumpbin.exe /exports hoge.lib Microsoft (R) COFF/PE Dumper Version 8.00.50727.762 Copyright (C) Microsoft Corporation. All rights reserved. Dump of file hoge.lib File Type: LIBRARY Exports ordinal name ?funcHoge@@YAPAVint@@XZ (int __cdecl funcHoge(int)) |
出力の一部は上記のような感じになっています。funcHogeという関数名が
コンパイルされると上表の最下行のような名前に変わっています。付け加わった文字列が
装飾名と呼ばれるものでC++のオーバーロードによるものです。C++では同名の関数名で
複数の関数が作成できるためにコンパイラが一定のロジックで文字列を加えるものです。
ですから、EXE側でGetProcAddressを使うためには取得する関数ポインタの名前に
この装飾名付の関数名をしていしないといけないです。
装飾名は関数のオーバーロード機能があるためなので、 前表のようにextern "C"を記述して「C言語としてコンパイルして オーバーロードはしません。」と宣言します。ということで、明示的リンクで__declspec(dllexport)を 使って関数をエクスポートするときは次のようにします。
// -------- hoge.h - DLL--------------- #ifdef __cplusplus exetern "C" { #endif declspec(dllexport) int funcHoge(int n); #ifdef __cplusplus } #endif // -------- hoge.cpp - DLL ------------ extern "C" int funcHoge(int n) { return 0; } |
これは関数に限ったことで、もしクラスをエクスポートするときは 関数へのポインタを取得する明示的リンクは使わないし、というよりクラスでCリンケージは あり得ないので考える必要はないでしょう。
declspec(dllexport)を使ってのクラスのエクスポートに関してもう少し考えて みます。クラスのエクスポートはそのメンバ関数のエクスポートです。DLLにクラスを つくってエクスポートしてdumpbin.exeでみてみると以下のようになります。
// ------------------ DLL ヘッダファイル-----------------
class HOGE_API CHoge
{
public:
CHoge(); //コンストラクタ
~CHoge(); //デストラクタ
virtual void func1();
int func2();
protected:
int m_nData1;
};
//------------------- dumpbin.exe ------------------------
C:\>dumpbin.exe /exports hogehoge.lib
Microsoft (R) COFF/PE Dumper Version 8.00.50727.762
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file hoge.lib
File Type: LIBRARY
Exports
ordinal name
??0CHoge@@QAE@ABV0@@Z (public: __thiscall CHoge::CHoge(class CHoge const &))
??0CHoge@@QAE@XZ (public: __thiscall CHoge::CHoge(void))
??1CHoge@@QAE@XZ (public: __thiscall CHoge::~CHoge(void))
??4CHoge@@QAEAAV0@ABV0@@Z (public: class CHoge & __thiscall CHoge::operator=(class CHoge const &))
??_7CHoge@@6B@ (const CHoge::`vftable')
?func1@CHoge@@UAEXXZ (public: virtual void __thiscall CHoge::func1(void))
?func2@CHoge@@QAEHXZ (public: int __thiscall CHoge::func2(void))
ダンプされた内容を見てみるとヘッダで宣言されたコンストラクタ、デストラクタ、
func1, func2に加えて、コンパイラが自動的に作るコピーコンストラクタ、代入演算子
オーバーロード関数、仮想関数テーブルポインタがエクスポートされています。特定のメンバを
呼び出させたくない場合は、クラス全体をエクスポートするのではなく、
以前に示したように個々のメンバに__declspec(dllexport)を
適用すればいいですが、気をつけたいのはコンストラクタ、デストラクタをエクスポートしないと
クラスオブジェクトは構築されないので注意してください。
また、仮想関数ついてですが上表でクラスをエクスポートを
やめてfunc1, func2にdeclspec(dllexport)を適用してください。この場合CHoge::func1,
CHoge::func2がエクスポートできることは確認できますが、仮想関数であるfunc1の
declspec(dllexport)を取り除いてDLLを使ってみてください。EXE側からは使えないはずのfunc1が
使えてしまいます。たぶん、仮想関数は関数名にほとんど意味がなく、vtable上の順番によって
コールされるためだと思いますが詳細な仕組みはわかりません。
(2. .defファイル(モジュール定義ファイル)のEXPORTSでのエクスポート)
2番目のエクスポートの方法は.defファイル(モジュール定義ファイル)を作成することです。
この方法は新しいファイルを作らないといけいないですが、一度作成して書き方を覚えれば、
装飾名を気にせずに簡単にエクスポートできます。モジュール定義ファイルは次のように
作成します。
ツールバーのプロジェクト(P)→新しい項目の追加(W)で 「新しい項目の追加」ダイアログを表示させてください。
カテゴリ : Visual C++ - コード テンプレート : モジュール定義ファイル(.def) ファイル名 : (任意 ex. hoge.def) →追加(A)
モジュール定義ファイルのスケルトンが出来ているので 以下のように加筆してください。
;------ hoge.def----------------------------- LIBRARY "hoge" EXPORTS func1 @1 func2 @2 //----- hoge.h ------------------------------- int func1(char *p); void func2(int n); //------ hoge.cpp ---------------------------- int func1(char *p) { return 0; } void func2(int n) {} |
上表に書いてあるとおり、"EXPORTS"というキーワードの後に関数名と
序数値(省略可)を記述しています。序数値はエクスポート用に関数名の代わりに数値を
ライブラリファイルなどに記録される、アットマーク(@)に続く数値です。
(記録される先をエクスポートテーブルといいます)。
これはDLLのサイズを小さくするためです。
モジュール定義ファイルを使うと、declspec(dllexport)とは異なり、ヘッダ・実装各ファイルには
何も手を加える必要はありません。
(#pragmaディレクティブでリンカに/EXPORTオプションを指定)
この方法はモジュール定義ファイルを作成する方法に似ていますが、違うのはこのファイルを
作らずにC++コードに#pragmaディレクティブでエクスポートする点です。記述方法は次のとおりです。
// --------- hoge.h ------------------- int fnHoge(); // --------- hoge.cpp ----------------- #pragma comment(linker, "/EXPORT:?fnHoge@@YAHXZ,@1") int fnHoge() { return 0; } |
新しいファイルを作らなくていいので管理が楽ですが、この方法は見てのとおり
装飾名を指定しないといけないので、いったんdeclspec(dllexport)でエクスポートした
DLLをdumpbin.exeでダンプして装飾名を確認しないといけません。
それよりもさらに問題なのはDLLを使うEXE側で明示的リンクを使う場合、EXE側でも
GetProcAddressで関数名を装飾名付で指定しないといけない点です。これでしたら、
最初からクラスのエクスポートもできるdeclspec(dllexport)を使ったほうがいいでしょう。
ということで、この方法はほとんど見かけないです。
<クラスのエクスポート時の注意点>
DLLの使い方、実装について、ごくごく基本的なことをそれぞれDLL側とそれを使う EXE側から書きましたが、その他の注意点などで結構重要なことをここで記しておきます。
DLLのメリットのひとつにアップグレードが簡単という点があります。ある
ルーチンをDLLに収めてEXEがそれらのルーチンを利用するような状況の場合、DLLをアップグレード
して、EXEはリビルドすることなしにDLLの差し替えだけで済みます。
しかし、これはDLL側が関数を提供してる場合で、クラスを提供している場合はそう単純ではないです。
DLLで関数を提供(エクスポート)している場合、DLLのアップグレードをするときは
エクスポートする関数のシグネチャを変更しなければ何の問題もありません。つまり、関数名、
引数の型と数、戻値の型が変更されていなければいいということです。
では、クラスのエクスポートの場合はどうかというとメンバ関数の追加は
問題ないはずです(既存のメンバ関数の変更はNGです)。これはメンバ関数は実はクラス内に
あるのではなく、thisポインタを引数として渡してコールされるクラスがスコープの関数だからです。
一方データメンバを追加した場合は、単にDLLを差し替えるだけではだめで、EXE側もリビルドする
必要があります。
これではせっかくのDLLのメリットが一部損なわれてしまいます。まず、なぜ、アップグレードのために、
エクスポート対象クラスのデータメンバを追加してはだめなのでしょうか?
DLL | EXE |
// ---------- hoge.h (A)------------------
class declspec(dllexport) CHoge
{
public:
CHoge();
~CHoge();
virtual void func1();
int func2();
protected:
int m_nData1;
};
|
// ----------- HogeHoge.h ---------------
#pragma once
#include "hoge.h"
class CHogeHoge
{
protected:
CHoge m_hoge;
};
|
↓↓↓DLLのみアップグレード↓↓↓ | |
// ---------- hoge.h (B)------------------ class declspec(dllexport) CHoge { public: CHoge(); ~CHoge(); virtual void func1(); int func2(); protected: int m_nData1; int m_nData2; }; |
上表を見てください。最初にDLLを(A)のようにCHogeを定義して作成します。
このhoge.hをEXE側でインクルードして使用します。その後、DLLをアップグレードするために
int型のm_nData2をメンバとして加えた上ビルドします。これをEXEをリビルドすることなく
DLLのみ差し替えるとEXE側でどういうことがおきるでしょう?
EXE側のhoge.hはm_nData2を認識してないので、そのサイズを4バイト分小さく誤解しています。
EXE側のスタックがその4バイト分少なく領域が確保されますが、使用される領域は4バイト分
多いですからスタックが壊れます。ですから、DLLをアップグレードしたらEXEも
hoge.hを再度インクルードしてリビルドする必要があります。
このようなリビルドを防ぐためにインターフェイスクラスを通してクラスを エクスポートするようにします。
DLL | EXE |
// ----- Hoge.h (インターフェイスクラス) --------- class CHoge { public: virtual void func1() = 0; virtual int func2() = 0; virtual void Destroy() = 0; }; インスタンス作成 extern "C" declspec(dllexport) CHoge * CreateCHogeInstance(); // ------------ Hoge.cpp -------------------- #include "HogeImpl.h" extern "C" CHoge * CreateCHogeInstance() { return new CHogeImpl; } // ----- HogeImpl.h (実装クラス)---------- #pragma once #include "Hoge.h" class CHogeImpl : public CHoge { public CHogeImpl(); virtual ~CHogeImpl(); /*virtual*/ void func1(); /*virtual*/ int func2(); /*virtual*/ void Destroy(); private: int m_nData1; }; // ------------ HogeImpl.cpp ------------------ #include "HogeImpl.h" CHogeImpl::CHogeImpl() : m_nData(0) { } CHogeImpl::~CHogeImpl() { } void CHogeImpl::func1() { // 必要な実装 } int CHogeImpl::func2() { // 必要な実装 return 0; } void CHogeImpl::Destroy() { delete this; } |
// --------- Test.h -------- class CHoge; //前方宣言 class CTest { private: CHoge *m_pHoge; }; // --------- Test.cpp --------- #include "Test.h" #include "Hoge.h" #pragma comment(lib, "Hoge.lib") m_pHoge = CreateCHogeInstance(); m_pHoge->func1(); m_pHoge->func2(); m_pHoge->Destroy(); |
上表をご覧ください。DLL側では、CHogeを純粋仮想関数のみのインターフェイスクラス として定義して、CHogeから実装クラスであるCHogeImplを派生します。CHogeImplに必要なデータメンバや 実装を施します。EXE側でインスタンスの作成ができるようにCreateCHogeInstanceをエクスポート します。また、インスタンスの破壊としてDestroy関数も用意します。
EXE側ではDLLのリンクとインターフェイスクラスであるHoge.hのみをインクルードします。 CreateCHogeInstanceでインスタンスをつくり、任意のメンバ関数を呼んで作業をしたのち、Destroy関数 でインスタンスを破壊します。なお、EXE側ではCHogeImpleがまったく見えてなく、そのサイズも 不明なため、delete演算子を使わないでCHogeで用意したDestroy関数でインスタンスを破壊します。 これで、わざわざCHogeにインスタンス破壊用のメンバ関数を用意したのかわかると思います。
DLL側でアップグレードが必要になったときは、EXE側ではCHogeしか見えていないので、 CHogeさえ変更しなければ、CHogeImplを自由に変更しても問題ないです。つまり、CHogeImplだけを 変更していればEXE側はリビルドの必要はなく、DLLだけ差し替えれば正常に動作するはずです。
<DLL内のデータ共有>
DLLは起動しているプロセスの要請に応じてそのプロセス内にロードされます。 DLLのインスタンスはひとつですが、そのDLLを使う複数のプロセスのアドレス空間に割り当てられます。 ということは、DLL内のグローバル変数がそれらのプロセスで共有することができそうですが、 そのままではうまくいきません。というのは、データはプロセスごとにインスタンスが作成されて 独立しているからです。当然といえば当然です。アドレス空間はプロセスが管理しているので、 安易にプロセスを越えてデータが行き来しては困ります。
どのようにプロセス間でDLLのデータを共有するかを書く前に、DLLのデータ (グローバル変数・静的変数)がどのように内部に格納されているかを確認します。DLLの中身を 見るのは先ほど紹介したdumpbin.exeツールです。任意のDLLの中身を 調べるのはコンソールで
C:\>dumpbin.exe /all xxxx.dll
とすると内容が表示されます。最後に表示されている部分を抜粋すると 以下のようです。
Summary 1000 .data 1000 .idata 2000 .rdata 1000 .reloc 1000 .rsrc 5000 .text 10000 .textbss
右側のドット(.)で始まる文字列はセクション名です。セクションとは ファイル内におけるサイズ制限のない連続のメモリブロックのことです。左側の数値は そのセクションのサイズです。DLL内のデータまたはコードはいずれかのセクションに格納されて いますが、初期化したグローバル変数は既定では.dataセクションに納められています (未初期化変数は.bssに格納)。つまり、
// -------- xxxxx.cpp -----------------
#include "xxxxx.h"
int g_nData = 1;
// ・
// ・
のようなコードがあるとき、これをコンパイルするとg_nDataは.dataセクションに 収められています(0で初期化すると未初期化として.bssに格納されます)。これら変数がどの セクションに納められているかということは、表面上使う分にはあまり関係ないように思われ ますが、各セクションには属性が存在していて、収められているセクションによって 変数が影響を受けています。dumpbinで出力した内容をセクションごとに 見ていくと次のような属性があることがわかると思います。(出力では示されないものもあるかも しれません)
文字 | 属性 | 説明 |
E | Execute | 実行可能 |
R | Read | データ読み取り可能 |
W | Write | データ書き込み可能 |
S | Shared | DLLを読み込んだすべてのプロセスで共有 |
D | Discardable | セクションを破棄可能としてマーク |
K | Cacheable | セクションをキャッシュ不可としてマーク |
P | Pageable | セクションをページング不可としてマーク |
例えば.rdataセクションはWrite属性がなく、ここに収められているデータは読み取り 専用ということになります。また、.dataセクションはRead Write属性なので読み書き可能で、 .textはExecute Readなので実行可能なコードが含まれています。
ここで本題のプロセス間でDLLのデータを共有する方法について話を戻します。
ここまで書けばだいたいわかると思いますが、上記の属性でShared属性を持ったセクションに
データを格納すればDLLを読み込んだすべてのプロセスで共有が可能になります。
セクションは.dataや.bss、.rdataなど標準で備わっているものがあり、これらの属性を
勝手に変えてはいけません。.dataセクションが突如Read Onlyになったらたいへんです。そこで
解決策として固有のセクションを作って、そこにデータを格納した後、その属性を共有可能に
するという方法をとります。その方法は#pragmaディレクティブを使用して以下のような
記述をすることになります。
// --------DLL - xxxx.cpp ---------------- #pragma data_seg(".MyShare") //.MyShareセクションを作成&アクティブ化 int g_nData = 0; //.MyShareセクションにグローバル変数g_nDataを0で初期化して格納 #pragma data_seg() //現在のセクションを.dataにリセットする // .MyShareセクションの属性を Read, Write, Shareに変更 #pragma comment(linker, "/SECTION:.MyShare,RWS")
この記述によってg_nDataは、デフォルトでは各プロセスで新しいインスタンスが 作成されるところを、このDLLを使用しているすべてのプロセスで共有されることになります。
セクションの属性は#pragma commentで設定していますが、別の方法としてdefファイル (モジュール定義ファイル)を使う方法もあります。上の#pragma comment行をコメントアウト した上でエクスポート用のdefファイルを作成したようにdefファイルを 作成して以下のように書き込んでください。
; .defファイル
SECTIONS
.MyShare READ WRITE SHARED
これは余談ですが、#pragma data_segの説明はは英文でSpecifies data segment〜〜と 記されています。このプラグマディレクティブは「データセグメントを指定する」と いっているのですが、これまでの説明では「セクションの指定」をしています。 セグメントとセクションの違いはほとんどありません。ここではPEファイルではセクション、 NEファイルではセグメントと呼んでいることだけ覚えておけばいいかと思います。 dumpbinを/allオプションでdllの中身を表示してみると上のほうで 「PE signature found」と表示されているので、data_segではセクションを指定することになります。
これでDLLを使用するプロセスすべてでデータを共有することができますが、 注意点がいくつかあるので覚えておいてください。
ポインタはデータのあるアドレス値であり、アドレスはプロセスで管理しているため、
複数のプロセスで共有されるデータは別のプロセスでは無効になってしまいます。
これを踏まえると仮想関数を含むクラスもだめです。仮想関数を含むクラスにはvtableポインタが
C++によって生成されるので、このポインタは無効になる可能性があるためです。また、staticな
データをメンバとしたクラスもだめです。staticデータメンバはグローバルと同等として扱われる
ため、本来なら1つしか許されないstaticデータメンバが上表のように複数のプロセスでコピーが
作成されてしまうからです。
このような状況からDLLでデータを共有する場合はC++オブジェクトやポインタは さけてプリミティブなデータ型、又はプリミティブなデータ型をメンバとする構造体にする のが吉かと思います。
DLLはテーマとしては広いのでほんの一部しか書いていませんが、本題が フックについてなので、それに必要なことだけとりあえず 書きました。これで前提がそろったので、実際にシステムフック例を掲載します。