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 枚の絵になります。