次の方法で共有


混在アセンブリの初期化

Visual C++ .NET および Visual C++ 2003 では、/clr コンパイラ オプションを指定してコンパイルされた DLL は、読み込み時に非確定的にデッドロックを生じる可能性があります。この問題は、混在モード DLL 読み込み時の問題 (またはローダー ロックの問題) と呼ばれていました。 混在モード DLL の読み込みプロセスで、このような確定的でない場合の問題はほとんどなくなりました。 ただし、ローダー ロックが (確定的に) 発生する可能性のあるシナリオはいくつか残っています。 この問題の詳細については、MSDN Libraryの「Mixed DLL Loading Problem」を参照してください。

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

注意

Visual C++ 2003 では、デッドロックの発生を最小限に抑える一方で、DLL を簡単に初期化できるように、_vcclrit.h が用意されていました。 _vcclrit.h を使用する必要はありません。_vcclrit.h を使用すると、使用が推奨されていないことを示す警告がコンパイル中に表示されます。 そのため、「How To: Remove Dependency on _vcclrit.h」に記載されている手順を使用してこのファイルへの依存関係を削除することをお勧めします。 この他に、_vcclrit.h をインクルードする前に _CRT_VCCLRIT_NO_DEPRECATE を定義して警告が表示されないようにしたり、単にこの警告を無視したりする方法もありますが、理想的な解決策とはいえません。

ローダー ロックの原因

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

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

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

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

  • 2 番目のシナリオでは、.NET Framework Version 1.0 および 1.1 で DLL を読み込んだ場合に、CLR では、ローダー ロックが保持されていないと想定し、ローダー ロックの状況では無効な処理が複数実行されました。 ローダー ロックが保持されていないという想定は、純粋な .NET DLL では妥当な想定ではありません。ただし、混在モード DLL はネイティブな初期化ルーチンを実行するため、ネイティブな Windows ローダーが必要となり、その結果、ローダー ロックが発生します。 したがって、開発者が DLL の初期化中に MSIL 関数を実行しない場合でも、.NET Framework Version 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

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 バージョンを呼び出します。

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

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

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

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

カスタム ロケール

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

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

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

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

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

診断に対する障害

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

ヘッダーでの実装

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

Visual C++ .NET および Visual C++ .NET 2003 では、リンカーは単にこのように意味的に等しい定義の多くを選択し、ソース ファイルによって異なる最適化オプションが使用されている場合に事前宣言およびシナリオを調整します。 これにより、ネイティブ DLL と .NET DLL が混在する問題が発生します。

/clr が有効な CPP ファイルと無効な CPP ファイルの両方に同じヘッダーが含まれる場合があります。また、#include が #pragma unmanaged ブロック内でラップされる可能性もあるため、ヘッダーでの実装が用意されている MSIL バージョンの関数とネイティブ バージョンの関数を使用できます。 MSIL の実装とネイティブの実装は、ローダー ロックの状態での初期化については異なる意味を持ちます。これは、実際には、単一定義規則に違反しています。 その結果、リンカーが最大規模の実装を選択すると、#pragma unmanaged ディレクティブを使用して明示的にネイティブ コードにコンパイルした場合でも、MSIL バージョンの関数を選択できます。 MSIL バージョンのテンプレートまたはインライン関数がローダー ロックが発生しているときに呼び出されないようにするには、ローダー ロックの状況で呼び出されるこのような各関数のすべての定義を、#pragma unmanaged ディレクティブで修飾する必要があります。 サードパーティのヘッダー ファイルの場合、これを実現するための最も簡単な方法は、問題のヘッダー ファイルの #include ディレクティブの近くに #pragma unmanaged ディレクティブをプッシュしてポップすることです。 例については、「managed, 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();
}
  • Itanium を対象とするコンパイルの場合。すべての関数ポインターの実装にバグが存在します。 上のコード スニペットにおいて、仮に myObject_p が during_loaderlock() 内でローカルに定義されていた場合、呼び出しもマネージ実装に解決されます。

デバッグ モードでの診断

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

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

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

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

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

    これは、次の 2 つの方法で行うことができます。 1 つ目の方法として、mscoree.dll と mscorwks.dll の PDB をシンボル検索パスに追加します。 これを行うには、シンボル検索パスのオプションのダイアログ ボックスを開きます。 このダイアログ ボックスを開くには、[ツール] メニューの [オプション] をクリックします。 次に、[オプション] ダイアログ ボックスの左ペインの [デバッグ] ノードを展開し、[シンボル] をクリックします。 mscoree.dll PDB ファイルと mscorwks.dll PDB ファイルのパスを検索一覧に追加します。 これらの PDB は、%VSINSTALLDIR%\SDK\v2.0\symbols にインストールされます。 [OK] をクリックします。

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

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

    これを設定するには、ソリューションのスタートアップ プロジェクトの [プロパティ] グリッドを開きます。 [構成プロパティ] サブツリーで、[デバッグ] ノードを選択します。 [デバッガーのタイプ] フィールドを [ネイティブのみ] に設定します。

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

  4. /clr を使用した診断が生成されたら、[再試行] をクリックし、[中断] をクリックします。

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

  6. イミディエイト ウィンドウを開きます。このウィンドウを開くには、[デバッグ] メニューの [ウィンドウ] をクリックし、[イミディエイト] をクリックします。

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

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

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

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

説明

次のサンプルは、コードを 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 does not 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 does not throw away unused object.

参照

概念

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