混在アセンブリの初期化

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

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

ローダー ロックの原因

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

.NET コンストラクトのみを含むアセンブリがプロセスに読み込まれると、CLR ローダーは、必要なすべての読み込みタスクと初期化タスクを実行できます。 ただし、ネイティブコードとデータを含むことができる混在アセンブリを読み込むには、Windows ローダーも使用する必要があります。

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

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

  • 2つ目は、.NET Framework のバージョン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 バージョンを呼び出します。

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

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

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

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

カスタム ロケール

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

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

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

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

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

診断に対する障害

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

ヘッダーでの実装

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

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

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

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

  • インライン関数の呼び出しは、グローバルな静的関数ポインターを介して行われます。 このシナリオは、仮想関数はグローバル関数ポインターを介して呼び出されるため、isn'table になります。 たとえば、次のように入力します。
#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 イミディエイトのデバッグ] を選択 > Windows > Immediateします)。

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

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

  9. _CorDllMain ( DllMain 問題が発生した場合) または _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.

関連項目

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