混在アセンブリの初期化

Windows 開発者は、DllMain の間にコードを実行するとき、ローダー ロックに常に注意する必要があります。 ただし、それ以外にも、C++/CLI の混在モード アセンブリを扱うときに考慮する必要がある問題がいくつかあります。

DllMain 内のコードで、.NET 共通言語ランタイム (CLR) にアクセスしてはなりません。 つまり、DllMain は、直接的にも間接的にもマネージド関数を呼び出すことができません。DllMain でマネージド コードを宣言または実装してはなりません。また、DllMain 内では、ガベージ コレクションや自動ライブラリ読み込みは行われません。

ローダー ロックの原因

.NET プラットフォームの導入に伴い、実行モジュール (EXE または DLL) を読み込むためのメカニズムが 2 つになりました。1 つは、Windows 向けで、アンマネージド モジュールに使用されます。もう 1 つは、CLR 向けで、.NET アセンブリを読み込みます。 混在モード DLL 読み込み時の問題は、Microsoft Windows OS ローダーを中心に発生します。

.NET のコンストラクトだけを含むアセンブリをプロセスに読み込んだ場合、CLR ローダーは、必要なすべての読み込みタスクおよび初期化タスクを自分で実行できます。 しかし、ネイティブ コードやデータが含まれている可能性がある混在アセンブリを読み込むには、Windows ローダーも使う必要があります。

Windows ローダーにより、初期化される前の DLL 内のコードやデータにコードでアクセスできないことが保証されます。 また、部分的な初期化中にコードで DLL を冗長に読み込むことができないことも保証されます。 そのため、Windows ローダーでは、モジュールの初期化中に安全ではないアクセスを防止する、プロセス グローバルのクリティカル セクション (多くの場合 "ローダー ロック" と呼ばれています) が使用されます。 結果として、読み込みプロセスでは、典型的な多くのデッドロックのシナリオが発生しやすくなります。 混在アセンブリの場合、次の 2 つのシナリオで、デッドロックの危険性が高くなります。

  • 最初のシナリオでは、DllMain または静的初期化子などでローダー ロックが保持されている場合に、Microsoft 中間言語 (MSIL) にコンパイルされた関数をユーザーが実行しようとすると、デッドロックが発生する可能性があります。 MSIL 関数が、まだ読み込まれていないアセンブリ内の型を参照する場合を考えてみてください。 CLR は、そのアセンブリを自動的に読み込もうとします。これにより、Windows ローダーはローダー ロックをブロックすることが必要になる場合があります。 呼び出しシーケンスのより早い時点でコードが既にローダー ロックを保持しているため、デッドロックが発生します。 ただし、ローダー ロック中に MSIL を実行しても、必ずデッドロックが発生するわけではありません。 そのため、このシナリオの診断と修正が困難になります。 参照されている型の DLL にネイティブ コンストラクトが含まれておらず、そのすべての依存関係にネイティブ コンストラクトが含まれていない場合など、状況によっては、Windows ローダーは参照されている型の .NET アセンブリを読み込む必要がありません。 さらに、必要なアセンブリやその混在するネイティブまたは .NET の依存関係は、既に他のコードによって読み込まれている可能性があります。 その結果、デッドロックの発生を予測することが難しくなります。また、デッドロック状態が、対象となるコンピューターの構成によって異なる場合もあります。

  • 2 番目のシナリオでは、.NET Framework Version 1.0 および 1.1 の DLL を読み込むときに、CLR はローダー ロックが保持されていないと想定し、ローダー ロックの状況下では無効な処理をいくつか実行しました。 ローダー ロックが保持されていないと想定するのは、純粋な .NET DLL の場合は有効です。 ただし、混在モード DLL ではネイティブ初期化ルーチンが実行されるため、ネイティブの Windows ローダーが必要であり、その結果ローダー ロックが必要になります。 そのため、開発者が DLL の初期化中に MSIL 関数の実行を試みなかった場合でも、.NET Framework バージョン 1.0 および 1.1 では、非確定的なデッドロックが発生する可能性がわずかにありました。

混在モード DLL の読み込みプロセスで、このような確定的でない場合の問題はなくなりました。 これは、以下の変更によって実現されました。

  • CLR が、混在モード DLL の読み込み時に誤った想定を行わなくなりました。

  • アンマネージド初期化とマネージド初期化が、2 つの独立した段階で実行されます。 最初にアンマネージド初期化が (DllMain によって) 実行され、その後、.NET でサポートされている .cctor というコンストラクトによってマネージド初期化が実行されます。 後者は、/Zl または /NODEFAULTLIB を使用している場合を除いて、ユーザーが意識する必要はありません。 詳しくは、「/NODEFAULTLIB (ライブラリを無視する)」と「/Zl (既定のライブラリ名の省略)」をご覧ください。

ローダー ロックは依然として発生することがありますが、再現性があり、検出されるようになりました。 DllMain に MSIL 命令が含まれている場合、コンパイラは、コンパイラ警告 (レベル 1) C4747 という警告を生成します。 さらに、ローダー ロックの状況下で MSIL が実行されようとしている場合、CRT または CLR は検出とレポートを試みます。 CRT による検出の結果、実行時の診断として C ランタイム エラー R6033 が発生します。

この記事の残りの部分では、MSIL がローダー ロックの状況で実行できる他のシナリオについて説明します。 それらの各シナリオでの問題の解決方法と、デバッグ手法について説明します。

シナリオと回避策

ローダー ロックが発生している場合でも、ユーザー コードが MSIL を実行できる状況がいくつかあります。 開発者は、そのような状況のそれぞれについて、ユーザー コードの実装が MSIL 命令を実行しないようにする必要があります。 以下では、最も一般的な事例で問題を解決する方法を考えつつ、すべての可能性について説明します。

DllMain

DllMain 関数は、DLL 用のユーザー定義のエントリ ポイントです。 ユーザーがそれ以外の関数を指定しない限り、プロセスやスレッドを DLL にアタッチするか、プロセスやスレッドを DLL からデタッチするたびに、 DllMain が呼び出されます。 この呼び出しは、ローダー ロックが保持されているときに行われる可能性もあるため、ユーザー指定の DllMain 関数は MSIL にコンパイルしないでください。 さらに、コール ツリー内で DllMain をルートにしている関数も、MSIL にコンパイルできません。 この問題を解決するには、DllMain が定義されているコード ブロックを、#pragma unmanaged で修飾する必要があります。 DllMain によって呼び出されるすべての関数にも、同じ処理を行う必要があります。

このような関数で、他の呼び出しコンテキスト用に MSIL 実装を必要とする関数を呼び出す必要がある場合には、同じ関数の .NET バージョンとネイティブ バージョンを重複して作成する方法を使用できます。

別の方法として、DllMain が不要な場合またはローダー ロックの状況でこれを実行する必要がない場合は、ユーザー指定の DllMain の実装を削除できます。そうすることで、問題が発生しなくなります。

DllMain で直接 MSIL を実行しようとすると、コンパイラ警告 (レベル 1) C4747 が発生します。 ただし、コンパイラは、DllMain が別のモジュール内の関数を呼び出し、その関数が MSIL を実行するような事例は検出できません。

このシナリオの詳細については、「診断に対する障害」を参照してください。

静的オブジェクトの初期化

静的オブジェクトを初期化すると、動的初期化子が必要な場合にデッドロックが発生することがあります。 単純なケース (コンパイル時に認識される値を静的変数に割り当てる場合など) では、動的な初期化が不要なため、デッドロックの危険性はありません。 ただし、一部の静的変数は、関数呼び出し、コンストラクター呼び出し、またはコンパイル時に評価できない式によって初期化されます。 これらの変数には、モジュールの初期化中に実行するコードが必要です。

動的な初期化を必要とする静的初期化子の例 (関数呼び出し、オブジェクト構築、およびポインター初期化) を次のコードに示します。 (これらの例は静的ではありませんが、グローバル スコープで定義されることが想定されるため、同じ結果になります)。

// dynamic initializer function generated
int a = init();
CObject o(arg1, arg2);
CObject* op = new CObject(arg1, arg2);

このデッドロックの危険性は、該当モジュールが /clr を指定してコンパイルされているかどうかと MSIL が実行されるかどうかによって決まります。 具体的には、静的変数が /clr なしでコンパイルされる場合 (または #pragma unmanaged ブロック内にある場合) に、その変数の初期化に必要な動的初期化子によって MSIL 命令が実行されると、デッドロックが発生することがあります。 これは、/clr を指定せずにコンパイルされたモジュールでは、DllMain によって静的変数の初期化が実行されるためです。 これに対して、/clr を指定してコンパイルされた静的変数は、アンマネージ初期化段階が完了してローダー ロックが解除された後に、.cctor によって初期化されます。

静的変数の動的な初期化が原因で発生するデッドロックに対する解決策は多数あります。 これらは、問題の解決に必要な時間の順に並べられています。

  • 静的変数を含むソース ファイルは、/clr でコンパイルできます。

  • 静的変数によって呼び出されるすべての関数は、#pragma unmanaged ディレクティブを使用してネイティブ コードにコンパイルできます。

  • 静的変数が依存するコードを手動で複製して、.NET バージョンとネイティブ バージョンを作成し、それぞれに異なる名前を指定します。 その後、開発者は、ネイティブな静的初期化子からネイティブ バージョンを呼び出し、それ以外の場所から .NET バージョンを呼び出します。

起動に影響を与える、ユーザーが指定した関数

起動時の初期化でライブラリが依存する、ユーザー指定の関数がいくつかあります。 たとえば、newdelete 演算子など、C++ の演算子をグローバルにオーバーロードすると、C++ 標準ライブラリの初期化や破棄など、すべての場所でユーザー指定バージョンが使用されます。 その結果、C++ 標準ライブラリおよびユーザー指定の静的初期化子は、これらの演算子のユーザー指定バージョンを呼び出します。

ユーザー指定バージョンが MSIL にコンパイルされると、これらの初期化子は、ローダー ロックが保持されているときに MSIL 命令を実行しようとします。 ユーザー指定の malloc でも、同じ結果になります。 この問題を解決するには、これらのオーバーロードまたはユーザー指定定義を、#pragma unmanaged ディレクティブを使用してネイティブ コードとして実装する必要があります。

このシナリオの詳細については、「診断に対する障害」を参照してください。

カスタム ロケール

ユーザーがグローバルなカスタム ロケールを指定すると、このロケールは、今後、すべての入出力ストリームの初期化に使用されます。静的に初期化されるストリームも対象になります。 このグローバルなロケール オブジェクトを MSIL にコンパイルすると、MSIL にコンパイルされたロケール オブジェクト メンバー関数が、ローダー ロックが保持されているときに呼び出されることがあります。

この問題を解決するためのオプションが 3 つあります。

グローバル入出力ストリームの定義をすべて含むソース ファイルは、/clr オプションを使用してコンパイルできます。 これにより、ローダー ロックの状態では静的初期化子が実行されなくなります。

カスタム ロケールの関数定義は、#pragma unmanaged ディレクティブを使用することによって、ネイティブ コードにコンパイルできます。

ローダー ロックが解除されるまで、カスタム ロケールをグローバル ロケールとして設定しないようにします。 その後、初期化中に作成された入出力ストリームをカスタム ロケールで明示的に構成します。

診断に対する障害

場合によっては、デッドロックの原因を検出することが困難になります。 以下では、そのようなシナリオとそれらの問題の解決策について説明します。

ヘッダーでの実装

特殊なケースで、ヘッダー ファイル内に関数を実装すると、診断が困難になる場合があります。 インライン関数とテンプレート コードの両方で、その関数をヘッダー ファイルに指定する必要があります。 C++ 言語では、単一定義規則を指定します。単一定義規則を指定すると、同じ名前で実装されているすべての関数が、強制的に同じ意味にされます。 その結果、C++ リンカーでは、特定の関数を重複して実装しているオブジェクト ファイルをマージする際に特別に注意する必要がなくなります。

Visual Studio 2005 より前のバージョンのリンカーでは、これらの意味的に同等の定義の中で最も大きいものが単純に選ばれます。 これは、事前宣言と、ソース ファイルによって異なる最適化オプションが使用されるシナリオに対応するために行われます。 これにより、ネイティブ DLL と .NET DLL が混在する問題が発生します。

/clr が有効な C++ ファイルと無効な C++ ファイルの両方に同じヘッダーがインクルードされたり、#include が #pragma unmanaged ブロック内にラップされたりすることがあるため、ヘッダーで実装が用意されている MSIL バージョンの関数とネイティブ バージョンの関数の両方が使用される可能性があります。 MSIL の実装とネイティブの実装は、ローダー ロックの状態での初期化については異なる意味を持ちます。これは、事実上、単一定義規則に違反しています。 その結果、リンカーが最大の実装を選択すると、他の場所で #pragma unmanaged ディレクティブを使用して明示的にネイティブ コードにコンパイルした場合でも、関数の MSIL バージョンが選択されます。 MSIL バージョンのテンプレートまたはインライン関数がローダー ロック中に呼び出されないようにするには、ローダー ロック中に呼び出されるこのような各関数のすべての定義を、#pragma unmanaged ディレクティブで修飾する必要があります。 サード パーティのヘッダー ファイルの場合、この変更を行う最も簡単な方法は、問題のヘッダー ファイルの #include ディレクティブの周囲で #pragma unmanaged ディレクティブをプッシュしてポップすることです。 (例については、マネージド、アンマネージドを参照してください)。ただし、この戦略は、.NET API を直接呼び出す必要がある他のコードを含むヘッダーでは機能しません。

ローダー ロックを扱うユーザーの負担を減らすため、マネージドとネイティブの両方の実装が存在する場合、リンカーはネイティブの実装を選択するようになっています。 この既定値により、上記の問題は回避されます。 ただし、このリリースではコンパイラに未解決の問題が 2 つ残っているため、この規則には、次の 2 つの例外があります。

  • グローバル静的関数ポインターを介したインライン関数呼び出しである場合。 仮想関数はグローバル関数ポインターを介して呼び出されるため、このシナリオには注意する必要があります。 たとえば、 にします。
#include "definesmyObject.h"
#include "definesclassC.h"

typedef void (*function_pointer_t)();

function_pointer_t myObject_p = &myObject;

#pragma unmanaged
void DuringLoaderlock(C & c)
{
    // Either of these calls could resolve to a managed implementation,
    // at link-time, even if a native implementation also exists.
    c.VirtualMember();
    myObject_p();
}

デバッグ モードでの診断

ローダー ロックに関する問題の診断はすべて、デバッグ ビルドで行う必要があります。 リリース ビルドで診断が生成されない場合があります。 また、リリース モードで行われる最適化によって、ローダー ロックのシナリオで MSIL の一部が隠されることがあります。

ローダー ロックに関する問題をデバッグする方法

MSIL 関数が呼び出されたときに、CLR が生成する診断は、CLR の実行を中断します。 さらに、このことが原因で、デバッグ対象をインプロセスで実行している場合は Visual C++ の混合モード デバッガーも中断されます。 ただし、プロセスにアタッチした場合、混合デバッガーを使用してデバッグ対象のマネージド コールスタックを取得することはできません。

ローダー ロック中に呼び出された特定の MSIL 関数を識別するには、開発者が次の手順を実行する必要があります。

  1. mscoree.dll と mscorwks.dll のシンボルを使用できるようにします。

    シンボルは 2 つの方法で使用できます。 1 つ目の方法として、mscoree.dll と mscorwks.dll の PDB をシンボル検索パスに追加します。 それらを追加するには、シンボル検索パスのオプションのダイアログ ボックスを開きます。 ([ツール] メニューの [オプション] を選択します。[オプション] ダイアログ ボックスの左側のウィンドウで、[デバッグ] ノードを開き、[シンボル] を選択します)。mscoree.dll ファイルと mscorwks.dll PDB ファイルへのパスを検索リストに追加します。 これらの PDB は、%VSINSTALLDIR%\SDK\v2.0\symbols にインストールされます。 OK を選択します。

    2 つ目の方法として、mscoree.dll と mscorwks.dll の PDB を Microsoft Symbol Server からダウンロードします。 Symbol Server を構成するには、シンボル検索パスのオプションのダイアログ ボックスを開きます。 ([ツール] メニューの [オプション] を選択します。[オプション] ダイアログ ボックスの左側のウィンドウで、[デバッグ] ノードを開き、[シンボル] を選択します)。次の検索パスを検索リストに追加しますhttps://msdl.microsoft.com/download/symbols。 シンボルのキャッシュ ディレクトリをシンボル サーバーのキャッシュのテキスト ボックスに追加します。 OK を選択します。

  2. デバッガーのモードをネイティブのみに設定します。

    ソリューションのスタートアップ プロジェクトの [プロパティ] グリッドを開きます。 [構成プロパティ]>[デバッグ] を選択します。 [デバッガーの種類] プロパティを [ネイティブのみ] に設定します。

  3. デバッガーを起動します (F5 キー)。

  4. /clr の診断が生成されたら、[再試行] を選んで、[中断] を選びます。

  5. [呼び出し履歴] ウィンドウを開きます。 (メニュー バーで、Windows>呼び出し履歴をデバッグ>します。)問題のあるDllMain初期化子または静的初期化子は、緑色の矢印で識別されます。 問題のある関数が示されない場合は、以下の手順を実行して検索する必要があります。

  6. [イミディエイト] ウィンドウを開きます (メニュー バーで、[デバッグ]>[Windows]>[イミディエイト] を選びます)。

  7. [イミディエイト] ウィンドウに「.load sos.dll」と入力し、SOS デバッグ サービスを読み込みます。

  8. [イミディエイト] ウィンドウに「!dumpstack」と入力し、内部 /clr スタックの完全な一覧を取得します。

  9. DllMain が原因の場合は _CorDllMain、静的初期化子が原因の場合は _VTableBootstrapThunkInitHelperStub または GetTargetForVTableEntry の最初のインスタンス (スタックの一番下にあるもの) を検索します。 この呼び出しのすぐ下にあるスタック エントリが、ローダー ロック中に実行しようとした、MSIL 実装の関数の呼び出しです。

  10. 前の手順で特定したソース ファイルと行番号に移動し、「シナリオ」セクションで説明したシナリオと解決策を使用して問題を解決します。

説明

次のサンプルは、コードを DllMain からグローバル オブジェクトのコンストラクターに移動することによってローダー ロックを回避する方法を示します。

このサンプルにはグローバル マネージド オブジェクトがあり、そのコンストラクターには、元は DllMain 内にあったマネージド オブジェクトが含まれています。 このサンプルの 2 番目の部分は、マネージド オブジェクトのインスタンスを作成して、初期化を実行するモジュール コンストラクターを呼び出し、アセンブリを参照します。

コード

// initializing_mixed_assemblies.cpp
// compile with: /clr /LD
#pragma once
#include <stdio.h>
#include <windows.h>
struct __declspec(dllexport) A {
   A() {
      System::Console::WriteLine("Module ctor initializing based on global instance of class.\n");
   }

   void Test() {
      printf_s("Test called so linker doesn't throw away unused object.\n");
   }
};

#pragma unmanaged
// Global instance of object
A obj;

extern "C"
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) {
   // Remove all managed code from here and put it in constructor of A.
   return true;
}

この例では、混在アセンブリの初期化での問題を示します。

// initializing_mixed_assemblies_2.cpp
// compile with: /clr initializing_mixed_assemblies.lib
#include <windows.h>
using namespace System;
#include <stdio.h>
#using "initializing_mixed_assemblies.dll"
struct __declspec(dllimport) A {
   void Test();
};

int main() {
   A obj;
   obj.Test();
}

このコードを実行すると、次の出力が生成されます。

Module ctor initializing based on global instance of class.

Test called so linker doesn't throw away unused object.

関連項目

混在 (ネイティブおよびマネージド) アセンブリ