DLL_PROCESS_ATTACH やコンストラクタの実行タイミングを調査する
次の処理の実行タイミングを調査します。
- EXE のロード
- DLL のロード
- DLL_PROCESS_ATTACH
- DLL_PROCESS_DETACH
- グローバル変数のコンストラクタ
- グローバル変数のデストラクタ
- メインルーチン
- mainCRTStartup
- _DllMainCRTStartup
動作確認環境
- Windows 11 Home 21H2
- Visual Studio Community 2019
結論
結論から書きます。
下図の青丸が起動時の処理順になります。
下図の赤丸が終了時の処理順になります。
以下は調査手順の説明です。
調査用プログラムについて
調査用に次のプログラムを作りました。
- MyMain.exe。クローバルなクラスインスタンスをひとつ作成。MyDll1.dll の関数を呼び出す。
- MyDll1.dll。クローバルなクラスインスタンスをひとつ作成。MyDll2.dll の関数を呼び出す。
- MyDll2.dll。クローバルなクラスインスタンスをひとつ作成。
ソースは次の通りです。
#include <stdio.h>
#include <windows.h>
#include "MyClass.h"
extern "C" void Func1(void);
MyClass c("MyMain");
int main()
{
printf("main 開始\n");
Func1();
printf("main 終了\n");
return 0;
}
#include <stdio.h>
#include <windows.h>
#include "MyClass.h"
extern "C" void Func2(void);
MyClass c1("MyDll1");
extern "C" __declspec(dllexport) void Func1(void)
{
printf("Func1\n");
Func2();
}
BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD dwReason, LPVOID lpReserved)
{
switch (dwReason)
{
case DLL_PROCESS_ATTACH:
printf("MyDll1 DLL_PROCESS_ATTACH\n");
break;
case DLL_THREAD_ATTACH:
printf("MyDll1 DLL_THREAD_ATTACH\n");
break;
case DLL_THREAD_DETACH:
printf("MyDll1 DLL_THREAD_DETACH\n");
break;
case DLL_PROCESS_DETACH:
printf("MyDll1 DLL_PROCESS_DETACH\n");
break;
}
return TRUE;
}
#include <stdio.h>
#include <windows.h>
#include "MyClass.h"
MyClass c2("MyDll2");
extern "C" __declspec(dllexport) void Func2(void)
{
printf("Func2\n");
}
BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD dwReason, LPVOID lpReserved)
{
switch (dwReason)
{
case DLL_PROCESS_ATTACH:
printf("MyDll2 DLL_PROCESS_ATTACH\n");
break;
case DLL_THREAD_ATTACH:
printf("MyDll2 DLL_THREAD_ATTACH\n");
break;
case DLL_THREAD_DETACH:
printf("MyDll2 DLL_THREAD_DETACH\n");
break;
case DLL_PROCESS_DETACH:
printf("MyDll2 DLL_PROCESS_DETACH\n");
break;
}
return TRUE;
}
#include <stdio.h>
#include <string.h>
class MyClass
{
private:
char name[100];
public:
MyClass(char *p)
{
strcpy_s(name, _countof(name), p);
printf("%s コンストラクタ\n", name);
}
~MyClass()
{
printf("%s デストラクタ\n", name);
}
};
VS2019 (x86) でビルドします。
C:\tmp> cl /MD /Od /Zi /LD MyDll2.cpp
Microsoft(R) C/C++ Optimizing Compiler Version 19.28.29913 for x86
......
/out:MyDll2.dll
......
C:\tmp> cl /MD /Od /Zi /LD MyDll1.cpp MyDll2.lib
Microsoft(R) C/C++ Optimizing Compiler Version 19.28.29913 for x86
......
/out:MyDll1.dll
......
C:\tmp> cl /MD /Od /Zi MyMain.cpp MyDll1.lib
Microsoft(R) C/C++ Optimizing Compiler Version 19.28.29913 for x86
......
/out:MyMain.exe
......
「MyMain.exe」(親)と、それが呼び出す「MyDll1.dll」(子)、さらにそれが呼び出す「MyDll2.dll」(孫)が生成されました。
実行します。
C:\tmp> MyMain.exe
MyDll2 コンストラクタ
MyDll2 DLL_PROCESS_ATTACH
MyDll1 コンストラクタ
MyDll1 DLL_PROCESS_ATTACH
MyMain コンストラクタ
main 開始
Func1
Func2
main 終了
MyMain デストラクタ
MyDll1 DLL_PROCESS_DETACH
MyDll1 デストラクタ
MyDll2 DLL_PROCESS_DETACH
MyDll2 デストラクタ
プログラム中に埋め込んだ printf から、次のことがわかります。
- main の実行前に DLL の初期化が、実行後に DLL の後始末が行われる。
- DLL の初期化は「孫→子」の順に、後始末は「子→孫」の順に行われる。
- 各 DLL ごとに、コンストラクタの実行後に DLL_PROCESS_ATTACH が呼び出され、DLL_PROCESS_DETACH 呼び出し後にデストラクタが実行される。
WinDbg 上で実行して詳細を確認
printf だけでは
- EXE のロード
- DLL のロード
- mainCRTStartup
- _DllMainCRTStartup
の情報が得られないので、WinDbg から実行します。オプションの「-xe cpr -xe ld」は、プロセス生成時と DLL のロード時の停止指示です。
C:\tmp> windbg -xe cpr -xe ld Mymain.exe
Microsoft (R) Windows Debugger Version 10.0.22000.194 X86
......
ModLoad: 00a60000 00a6c000 MyMain.exe
......
「MyMain.exe」をロードしたタイミングで止まりました。
続いて WinDbg の [Command] ウィンドウから次のコマンドを入力し、「mainCRTStartup」「_DllMainCRTStartup」(C ランタイムライブラリのエントリーポイント)実行時にも停止するよう指示します。DLL のブレークポイント設定に「bp」ではなく「bu」コマンドを使っているのは、これらの DLL がまだロードされていないためです。
0:000> bp MyMain!mainCRTStartup
*** WARNING: Unable to verify checksum for MyMain.exe
0:000> bu MyDll1!_DllMainCRTStartup
0:000> bu MyDll2!_DllMainCRTStartup
そして「g」コマンドを繰り返し実行し、処理の過程を見ていきます。
重要なポイントを抜き出してまとめた結果が以下です。
WinDbg> ModLoad: 00a60000 00a6c000 MyMain.exe
WinDbg> ModLoad: 70d10000 70d1c000 C:\tmp\MyDll1.dll
WinDbg> ModLoad: 6e6c0000 6e6cc000 C:\tmp\MyDll2.dll
WinDbg> MyDll2!_DllMainCRTStartup: (dwReason = 1 = DLL_PROCESS_ATTACH)
printf> MyDll2 コンストラクタ
printf> MyDll2 DLL_PROCESS_ATTACH
WinDbg> MyDll1!_DllMainCRTStartup: (dwReason = 1 = DLL_PROCESS_ATTACH)
printf> MyDll1 コンストラクタ
printf> MyDll1 DLL_PROCESS_ATTACH
WinDbg> MyMain!__scrt_common_main [inlined in MyMain!mainCRTStartup]:
printf> MyMain コンストラクタ
printf> main 開始
printf> Func1
printf> Func2
printf> main 終了
printf> MyMain デストラクタ
WinDbg> MyDll1!_DllMainCRTStartup: (dwReason = 0 = DLL_PROCESS_DETACH)
printf> MyDll1 DLL_PROCESS_DETACH
printf> MyDll1 デストラクタ
WinDbg> MyDll2!_DllMainCRTStartup: (dwReason = 0 = DLL_PROCESS_DETACH)
printf> MyDll2 DLL_PROCESS_DETACH
printf> MyDll2 デストラクタ
これを図示したのが、最初の 2 枚の絵になります。