Share via


Initialisierung gemischter Assemblys

Windows-Entwickler müssen beim Ausführen von Code während DllMainder Ausführung von Code immer vorsichtig sein. Es gibt jedoch einige zusätzliche Probleme, die beim Umgang mit C++/CLI-Assemblys im gemischten Modus berücksichtigt werden müssen.

Code in DllMain darf nicht auf die .NET Common Language Runtime (CLR) zugreifen. Dies bedeutet, dass DllMain keine Aufrufe von verwalteten Funktionen direkt oder indirekt erfolgen sollten; kein verwalteter Code sollte deklariert oder implementiert DllMainwerden; und es sollte keine Garbage Collection oder automatische Bibliotheksladevorgang erfolgen.DllMain

Ursachen für die Loadersperre

Mit der Einführung der .NET-Plattform gibt es zwei unterschiedliche Mechanismen zum Laden eines Ausführungsmoduls (EXE oder DLL): eines für Windows, das für nicht verwaltete Module verwendet wird, und eine für die CLR, die .NET-Assemblys lädt. Das Problem beim Laden gemischter DLLs konzentriert sich auf den Loader des Microsoft Windows-Betriebssystems.

Wenn eine Assembly, die nur .NET-Konstrukte enthält, in einen Prozess geladen wird, kann das CLR-Ladeprogramm alle erforderlichen Lade- und Initialisierungsaufgaben selbst ausführen. Um jedoch gemischte Assemblys zu laden, die systemeigenen Code und Daten enthalten können, muss auch das Windows-Ladeprogramm verwendet werden.

Das Windows-Ladeprogramm garantiert, dass kein Code vor der Initialisierung auf Code oder Daten in dieser DLL zugreifen kann. Außerdem wird sichergestellt, dass kein Code die DLL redundant laden kann, während sie teilweise initialisiert ist. Dazu verwendet das Windows-Ladeprogramm einen prozess-globalen kritischen Abschnitt (häufig als "Ladeprogrammsperre" bezeichnet), der den unsicheren Zugriff während der Modulinitialisierung verhindert. Daher ist der Ladevorgang für viele klassische Deadlockszenarien anfällig. Bei gemischten Assemblys erhöhen die folgenden zwei Szenarien das Deadlockrisiko:

  • Wenn Benutzer zunächst versuchen, funktionen auszuführen, die in microsoft Intermediate Language (MSIL) kompiliert wurden, wenn die Ladesperre (z. B. von DllMain oder in statischen Initialisierern) gehalten wird, kann dies zu Deadlock führen. Berücksichtigen Sie den Fall, in dem die MSIL-Funktion auf einen Typ in einer Assembly verweist, die noch nicht geladen wurde. Die CLR versucht, diese Assembly automatisch zu laden, was erfordern kann, dass das Windows-Ladeprogramm von der Loadersperre blockiert wird. Ein Deadlock tritt auf, da die Ladesperre bereits zuvor in der Aufrufsequenz von Code gehalten wird. Das Ausführen von MSIL unter Ladeprogrammsperre garantiert jedoch nicht, dass ein Deadlock auftritt. Das macht dieses Szenario schwierig zu diagnostizieren und zu beheben. In einigen Fällen, z. B. wenn die DLL des referenzierten Typs keine systemeigenen Konstrukte enthält und alle zugehörigen Abhängigkeiten keine systemeigenen Konstrukte enthalten, ist das Windows-Ladeprogramm nicht erforderlich, um die .NET-Assembly des referenzierten Typs zu laden. Darüber hinaus wurden die erforderliche Assembly oder ihre gemischten nativen/.NET-Abhängigkeiten möglicherweise bereits durch anderen Code geladen. Folglich kann die Vorhersage eines Deadlocks schwierig sein und abhängig von der Konfiguration des Zielcomputers variieren.

  • Zweitens: Beim Laden von DLLs in den Versionen 1.0 und 1.1 des .NET Frameworks hat die CLR davon ausgegangen, dass die Ladeprogrammsperre nicht gehalten wurde und mehrere Aktionen ausgeführt wurden, die unter ladeprogrammischer Sperre ungültig sind. Unter der Annahme, dass die Ladeprogrammsperre nicht gehalten wird, ist eine gültige Annahme für rein .NET DLLs. Da gemischte DLLs jedoch systemeigene Initialisierungsroutinen ausführen, benötigen sie das systemeigene Windows-Ladeprogramm und folglich die Ladeprogrammsperre. Selbst wenn der Entwickler während der DLL-Initialisierung keine MSIL-Funktionen ausführen wollte, gab es in .NET Framework-Versionen 1.0 und 1.1 immer noch eine kleine Möglichkeit, nicht deterministische Deadlocks auszuführen.

Der gesamte Nichtdeterminismus wurde aus dem Prozess des Ladens gemischter DLLs entfernt. Es wurde mit diesen Änderungen erreicht:

  • Die CLR geht beim Laden von gemischten DLLs nicht mehr von falschen Annahmen aus.

  • Die nicht verwaltete und verwaltete Initialisierung erfolgt in zwei separaten und unterschiedlichen Phasen. Die nicht verwaltete Initialisierung erfolgt zuerst (über DllMain) und die verwaltete Initialisierung erfolgt anschließend durch eine . NET-unterstütztes .cctor Konstrukt. Letzteres ist für den Benutzer vollständig transparent, es sei denn /Zl , sie /NODEFAULTLIB werden verwendet. Weitere Informationen finden Sie unter/NODEFAULTLIB (Bibliotheken ignorieren) und /Zl (Standardbibliotheksname weglassen).

Die Loadersperre kann zwar immer noch auftreten, jetzt aber reproduzierbar, und wird erkannt. Wenn DllMain MSIL-Anweisungen enthalten sind, generiert der Compiler warnungscompiler Warnung (Ebene 1) C4747. Darüber hinaus versucht die CRT oder die CLR, Versuche zu erkennen und zu melden, die MSIL unter der Loadersperre auszuführen. CRT-Erkennung führt zum Laufzeitdiagnose-C-Laufzeitfehler R6033.

Der Rest dieses Artikels beschreibt die Neu Standard szenarien, für die MSIL unter der Ladesperre ausgeführt werden kann. Es zeigt, wie Sie das Problem unter jedem dieser Szenarien beheben und Debuggingtechniken ausführen.

Szenarien und Problemumgehungen

Es gibt mehrere Situationen, in denen Benutzercode MSIL unter der Loadersperre ausführen kann. Der Entwickler muss sicherstellen, dass die Benutzercodeimplementierung unter jedem dieser Umstände nicht versucht, MSIL-Anweisungen auszuführen. In den folgenden Unterabschnitten werden alle Möglichkeiten mit einer Erläuterung der Problemlösung in den häufigsten Fällen beschrieben.

DllMain

Die DllMain Funktion ist ein benutzerdefinierter Einstiegspunkt für eine DLL. Wenn vom Benutzer nicht anders angegeben, wird DllMain jedes Mal aufgerufen, wenn ein Prozess oder Thread der enthaltenden DLL angefügt oder davon getrennt wird. Da dieser Aufruf auftreten kann, während die Loadersperre aktiv ist, sollte keine benutzerdefinierte DllMain -Funktion in MSIL kompiliert werden. Darüber hinaus kann keine Funktion in der Aufrufstruktur, die in DllMain wurzelt, in MSIL kompiliert werden. Um Probleme hier zu beheben, sollte der codeblock, der definiert DllMain wird, mit #pragma unmanaged. Das gleiche sollte für jede Funktion getan werden, die DllMain aufruft.

In Fällen, in denen diese Funktionen eine Funktion aufrufen müssen, die eine MSIL-Implementierung für andere Aufrufkontexte erfordert, können Sie eine Duplizierungsstrategie verwenden, bei der sowohl eine .NET- als auch eine systemeigene Version derselben Funktion erstellt werden.

Alternativ können Sie die vom Benutzer bereitgestellte DllMain Implementierung entfernen, DllMain wenn sie nicht erforderlich ist oder wenn sie nicht unter Ladeprogrammsperre ausgeführt werden muss.

Wenn DllMain versucht wird, MSIL direkt auszuführen, führt die Compilerwarnung (Ebene 1) C4747 dazu. Der Compiler kann jedoch keine Fälle erkennen, in denen DllMain eine Funktion in einem anderen Modul aufgerufen wird, das wiederum versucht, MSIL auszuführen.

Weitere Informationen zu diesem Szenario finden Sie unter "Impediments to Diagnostics".

Initialisieren von statischen Objekten

Initialisieren von statischen Objekten kann zu Deadlocks führen, wenn ein dynamischer Initialisierer erforderlich ist. Einfache Fälle (z. B. wenn Sie zur Kompilierungszeit einen Wert zuweisen, der zur Kompilierung einer statischen Variablen bekannt ist) erfordern keine dynamische Initialisierung, sodass kein Deadlock besteht. Einige statische Variablen werden jedoch durch Funktionsaufrufe, Konstruktoraufrufe oder Ausdrücke initialisiert, die zur Kompilierungszeit nicht ausgewertet werden können. Für diese Variablen ist code erforderlich, der während der Modulinitialisierung ausgeführt werden muss.

Der folgende Code zeigt Beispiele für statische Initialisierer, die dynamische Initialisierung erfordern: ein Funktionsaufruf, eine Objektkonstruktion und eine Initialisierung eines Zeigers. (Diese Beispiele sind nicht statisch, werden jedoch als Definitionen im globalen Bereich angenommen, die denselben Effekt haben.)

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

Dieses Risiko von Deadlock hängt davon ab, ob das enthaltende Modul kompiliert /clr und ob MSIL ausgeführt wird. Wenn die statische Variable ohne /clr (oder in einem #pragma unmanaged Block) kompiliert wird und der dynamische Initialisierer, der zum Initialisieren erforderlich ist, zur Ausführung von MSIL-Anweisungen führt, kann deadlock auftreten. Der Grund dafür ist, dass für Module, die ohne /clrkompiliert werden, die Initialisierung statischer Variablen von DllMain ausgeführt wird. Im Gegensatz dazu werden statische Variablen, die /clr kompiliert wurden, durch die .cctorInitialisierungsphase initialisiert, nachdem die nicht verwaltete Initialisierungsphase abgeschlossen wurde und die Ladeprogrammsperre freigegeben wurde.

Es gibt eine Reihe von Lösungen zum Deadlock, die durch die dynamische Initialisierung statischer Variablen verursacht werden. Sie sind hier ungefähr in der Reihenfolge der Zeit angeordnet, die erforderlich ist, um das Problem zu beheben:

  • Die Quelldatei, die die statische Variable enthält, kann mit /clrkompiliert werden.

  • Alle von der statischen Variablen aufgerufenen Funktionen können mithilfe der #pragma unmanaged Direktive in systemeigenen Code kompiliert werden.

  • Duplizieren Sie manuell den Code, von dem die statische Variable abhängig ist, sodass Sie eine .NET- und eine native Version mit unterschiedlichen Namen bereitstellen. Entwickler können dann die native Version der nativen statischen Initialisierer aufrufen und die .NET-Version an anderer Stelle.

Benutzerdefinierte Funktionen, die den Start beeinflussen

Es gibt mehrere benutzerdefinierte Funktionen, von denen Bibliotheken zur Initialisierung beim Start abhängig sind. Wenn z. B. global operatoren in C++ wie z. B. die und delete die new Operatoren global überladen werden, werden die vom Benutzer bereitgestellten Versionen überall verwendet, einschließlich der Initialisierung und Zerstörung der C++-Standardbibliothek. Daher rufen C++-Standardbibliotheken und vom Benutzer bereitgestellte statische Initialisierer alle vom Benutzer bereitgestellten Versionen dieser Operatoren auf.

Wenn die vom Benutzer bereitgestellten Versionen in MSIL kompiliert werden, versuchen diese Initialisierer, MSIL-Anweisungen ausführen, während die Loadersperre aktiviert ist. Ein vom Benutzer bereitgestellter malloc Benutzer hat die gleichen Folgen. Um dieses Problem zu beheben, muss jede dieser Überladungen oder vom Benutzer bereitgestellten Definitionen mithilfe der #pragma unmanaged Direktive als systemeigener Code implementiert werden.

Weitere Informationen zu diesem Szenario finden Sie unter "Impediments to Diagnostics".

Benutzerdefinierte Gebietsschemas

Wenn der Benutzer ein benutzerdefiniertes globales Gebietsschema bereitstellt, wird dieses Gebietsschema verwendet, um alle zukünftigen E/A-Datenströme zu initialisieren, einschließlich Datenströme, die statisch initialisiert werden. Wenn dieses globale Gebietsschemaobjekt in MSIL kompiliert wird, können in MSIL kompilierte Gebietsschemaobjekt-Memberfunktionen aufgerufen werden, während die Loadersperre aktiviert ist.

Es gibt drei Optionen zur Lösung dieses Problems:

Die Quelldateien, die alle globalen E/A-Datenstromdefinitionen enthalten, können mithilfe der /clr Option kompiliert werden. Sie verhindert, dass ihre statischen Initialisierer unter Ladeprogrammsperre ausgeführt werden.

Die definitionen der benutzerdefinierten Gebietsschemafunktion können mithilfe der #pragma unmanaged Direktive in systemeigenem Code kompiliert werden.

Legen Sie das benutzerdefinierte Gebietsschema nicht als globales Gebietsschema fest, solange die Loadersperre nicht freigegeben ist. Konfigurieren Sie dann explizit die während der Initialisierung mit dem benutzerdefinierten Gebietsschema erstellten E/A-Ströme.

Diagnosehindernisse

In einigen Fällen ist es schwierig, die Quelle von Deadlocks zu erkennen. In den folgenden Unterabschnitten werden diese Szenarien und die Möglichkeiten, diese Probleme zu umgehen, erörtert.

Implementierung in Headern

In bestimmten Fällen können Funktionsimplementierungen in Headerdateien die Diagnose erschweren. Inlinefunktionen und Vorlagencode erfordern, dass Funktionen in einer Headerdatei angegeben werden. Die Programmiersprache C++ gibt die One Definition Rule (Eine-Definition-Regel) an, die erzwingt, dass alle Implementierungen von Funktionen gleichen Namens semantisch gleichwertig sind. In der Folge muss der C++-Linker beim Zusammenführen von Objektdateien, die doppelte Implementierungen einer bestimmten Funktion aufweisen, nichts Spezielles berücksichtigen.

In Visual Studio-Versionen vor Visual Studio 2005 wählt der Linker einfach die größte dieser semantisch gleichwertigen Definitionen aus. Es wird getan, um Weiterleitungsdeklarationen aufzunehmen, und Szenarien, in denen unterschiedliche Optimierungsoptionen für verschiedene Quelldateien verwendet werden. Es verursacht ein Problem für gemischte systemeigene und .NET-DLLs.

Da derselbe Header sowohl von C++-Dateien mit /clr aktivierter als auch deaktivierter oder einer #include in einen #pragma unmanaged Block eingeschlossen werden kann, ist es möglich, sowohl MSIL- als auch systemeigene Versionen von Funktionen zu verwenden, die Implementierungen in Headern bereitstellen. MSIL- und systemeigene Implementierungen weisen unterschiedliche Semantik für die Initialisierung unter der Ladesperre auf, was effektiv gegen die eine Definitionsregel verstößt. Wenn der Linker daher die größte Implementierung wählt, kann er die MSIL-Version einer Funktion auswählen, auch wenn er explizit in systemeigenen Code kompiliert wurde, der die #pragma unmanaged Direktive verwendet. Um sicherzustellen, dass eine MSIL-Version einer Vorlage oder Inlinefunktion nie unter Ladesperre aufgerufen wird, muss jede Definition jeder solchen Funktion, die unter Ladeladesperre aufgerufen wird, mit der #pragma unmanaged Direktive geändert werden. Wenn die Headerdatei von einem Drittanbieter stammt, besteht die einfachste Möglichkeit, diese Änderung vorzunehmen, darin, die #pragma unmanaged Direktive für die #include Kopfzeilendatei zu pushen und aufzufüllen. (Ein Beispiel finden Sie unter "verwaltet" und "nicht verwaltet ".) Diese Strategie funktioniert jedoch nicht für Header, die anderen Code enthalten, der .NET-APIs direkt aufrufen muss.

Zur Erleichterung für Benutzer, die sich mit Loadersperren auseinandersetzen müssen, wählt der Linker die native Implementierung über die verwaltete Direktive, wenn er mit beiden konfrontiert wird. Diese Standardeinstellung vermeidet die oben genannten Probleme. In dieser Version gibt es jedoch zwei Ausnahmen für diese Regel, da zwei nicht behobene Probleme mit dem Compiler auftreten:

  • Der Aufruf einer Inlinefunktion erfolgt über einen globalen, statischen Funktionszeiger. Dieses Szenario ist tabelle, da virtuelle Funktionen über globale Funktionszeiger aufgerufen werden. Ein auf ein Objekt angewendeter
#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();
}

Diagnose im Debugmodus

Alle Diagnosen von Loadersperrenproblemen sollten mit Debugbuilds erfolgen. Releasebuilds erzeugen möglicherweise keine Diagnose. Und die im Releasemodus vorgenommenen Optimierungen können einige der MSIL-Szenarien unter Ladesperre maskieren.

So debuggen Sie Ladeprogrammsperrprobleme

Die Diagnose, die die CLR beim Aufruf einer MSIL-Funktion generiert, bewirkt, dass die CLR die Ausführung unterbricht. Dies wiederum bewirkt, dass der Debugger im gemischten Modus von Visual C++ beim Ausführen des Debuggee-In-Process angehalten wird. Beim Anfügen an den Prozess ist es jedoch nicht möglich, einen verwalteten Aufrufstack für den Debugvorgang mithilfe des gemischten Debuggers abzurufen.

Um die bestimmte MSIL-Funktion zu identifizieren, die unter der Loadersperre aufgerufen wurde, sollten Entwickler die folgenden Schritte ausführen:

  1. Stellen Sie sicher, dass für „mscoree.dll“ und „mscorwks.dll“ Symbole verfügbar sind.

    Sie können die Symbole auf zwei Arten verfügbar machen. Erstens können die PDBs für „mscoree.dll“ und „mscorwks.dll“ dem Symbolsuchpfad hinzugefügt werden. Um sie hinzuzufügen, öffnen Sie das Dialogfeld "Optionen für den Suchpfad" des Symbols. (Von der Menü "Extras", wählen Sie "Optionen" aus. Öffnen Sie im linken Bereich des Dialogfelds "Optionen" den Knoten "Debuggen", und wählen Sie "Symbole" aus.) Fügen Sie den Pfad zu den Dateien mscoree.dll und mscorwks.dll PDB zur Suchliste hinzu. Diese PDB-Dateien werden in den %VSINSTALLDIR%\SDK\v2.0\symbols installiert. Wählen Sie OK aus.

    Zweitens können die PDBs für „mscoree.dll“ und „mscorwks.dll“ vom Microsoft-Symbolserver heruntergeladen werden. Öffnen Sie zum Konfigurieren des Symbolservers das Dialogfeld für die Symbolsuchpfad-Optionen. (Von der Menü "Extras", wählen Sie "Optionen" aus. Öffnen Sie im linken Bereich des Dialogfelds "Optionen" den Knoten "Debuggen", und wählen Sie "Symbole" aus.) Fügen Sie diesen Suchpfad zur Suchliste hinzu: https://msdl.microsoft.com/download/symbols. Fügen Sie dem Symbolservercache-Textfeld ein Symbolcacheverzeichnis hinzu. Wählen Sie OK aus.

  2. Legen Sie den Debugmodus auf nur nativ fest.

    Öffnen Sie das Eigenschaftenraster für das Startprojekt in der Projektmappe. Wählen Sie Konfigurationseigenschaften>Debuggen aus. Legen Sie die Debuggertypeigenschaft auf "Native" fest.

  3. Starten Sie den Debugger (F5).

  4. Wenn die /clr Diagnose generiert wird, wählen Sie "Wiederholen" und dann "Abbrechen" aus.

  5. Öffnen Sie das Fenster „Aufrufliste“. (Wählen Sie auf der MenüleisteDebuggen des>Windows-Aufrufstapels>.) Der beleidigende DllMain oder statische Initialisierer wird mit einem grünen Pfeil identifiziert. Wenn die beleidigende Funktion nicht identifiziert wird, müssen die folgenden Schritte ausgeführt werden, um sie zu finden.

  6. Öffnen Des Direktfensters (Wählen Sie auf der Menüleiste "Windows>Direkt debuggen>" aus.)

  7. Geben Sie .load sos.dll in das Direktfenster ein, um den SOS-Debuggingdienst zu laden.

  8. Geben Sie !dumpstack in das Direktfenster ein, um eine vollständige Auflistung des internen /clr Stapels zu erhalten.

  9. Suchen Sie nach der ersten Instanz (die am unteren Rand des Stapels am nächsten ist) von entweder _CorDllMain (wenn DllMain das Problem verursacht wird) oder _VTableBootstrapThunkInitHelperStub oder GetTargetForVTableEntry (wenn ein statischer Initialisierer das Problem verursacht). Der Stapeleintrag direkt unterhalb dieses Aufrufs ist der Aufruf der MSIL-implementierten Funktion, die eine Ausführung unter der Loadersperre versuchte.

  10. Wechseln Sie zur im vorherigen Schritt angegebenen Quelldatei und zur Zeilennummer, und beheben Sie das Problem mithilfe der im Abschnitt "Szenarien" beschriebenen Szenarien und Lösungen.

Beispiel

BESCHREIBUNG

Das folgende Beispiel zeigt, wie Ladeprogrammsperre vermieden wird, indem Code in DllMain den Konstruktor eines globalen Objekts verschoben wird.

In diesem Beispiel gibt es ein globales verwaltetes Objekt, dessen Konstruktor das verwaltete Objekt enthält, das ursprünglich in DllMain. Der zweite Teil dieses Beispiels verweist auf die Assembly und erstellt eine Instanz des verwalteten Objekts, um den Modulkonstruktor aufzurufen, der die Initialisierung durchführt.

Code

// 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;
}

In diesem Beispiel werden Probleme bei der Initialisierung gemischter Assemblys veranschaulicht:

// 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();
}

Dieser Code erzeugt die folgende Ausgabe:

Module ctor initializing based on global instance of class.

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

Siehe auch

Gemischte (native und verwaltete) Assemblys