Dynamic-Link Bibliothek – Bewährte Methoden
**Aktualisiert: **
-
- Mai 2006
Wichtige APIs
Das Erstellen von DLLs stellt Entwickler vor eine Reihe von Herausforderungen. DLLs verfügen nicht über eine vom System erzwungene Versionsvererbung. Wenn mehrere Versionen einer DLL auf einem System vorhanden sind, führt die einfache Überschreiben von in Verbindung mit dem Fehlen eines Versionsschemas zu Abhängigkeits- und API-Konflikten. Die Komplexität in der Entwicklungsumgebung, der Loaderimplementierung und den DLL-Abhängigkeiten hat zu einer Zerbrechlichkeit in der Lade reihenfolge und im Anwendungsverhalten geführt. Schließlich verwenden viele Anwendungen DLLs und verfügen über komplexe Sätze von Abhängigkeiten, die für eine ordnungsgemäße Funktionsweise der Anwendungen zu berücksichtigen sind. Dieses Dokument enthält Richtlinien für DLL-Entwickler, die sie beim Erstellen robuster, portabler und erweiterbarer DLLs unterstützen.
Eine nicht ordnungsgemäße Synchronisierung in DllMain kann dazu führen, dass eine Anwendung deadlockt oder auf Daten oder Code in einer nicht initialisierten DLL zu zugreifen kann. Das Aufrufen bestimmter Funktionen innerhalb von DllMain verursacht solche Probleme.

Allgemeine bewährte Methoden
DllMain wird aufgerufen, während die Loadersperre gehalten wird. Daher gelten erhebliche Einschränkungen für die Funktionen, die in DllMain aufgerufen werden können. Daher ist DllMain so konzipiert, dass minimale Initialisierungsaufgaben mithilfe einer kleinen Teilmenge der Microsoft® Windows®-API ® Windows® werden. Sie können keine Funktion in DllMain aufrufen, die direkt oder indirekt versucht, die Ladesperre zu erhalten. Andernfalls besteht die Möglichkeit, dass Ihre Anwendung deadlocks oder abstürzt. Ein Fehler in einer DllMain-Implementierung kann den gesamten Prozess und alle seine Threads gefährden.
Die ideale DllMain wäre nur ein leerer Stub. Angesichts der Komplexität vieler Anwendungen ist dies jedoch im Allgemeinen zu restriktiv. Eine gute Faustregel für DllMain ist, so viel Initialisierung wie möglich zurückgestellt zu haben. Die verzögerte Initialisierung erhöht die Stabilität der Anwendung, da diese Initialisierung nicht ausgeführt wird, während die Ladesperre gehalten wird. Außerdem können Sie mithilfe der verzögerten Initialisierung einen größeren Teil der API sicher Windows verwenden.
Einige Initialisierungsaufgaben können nicht zurückgestellt werden. Beispielsweise sollte eine DLL, die von einer Konfigurationsdatei abhängt, nicht geladen werden, wenn die Datei falsch formatiert ist oder eine Garbage Collection enthält. Bei dieser Art der Initialisierung sollte die DLL die Aktion schnell versuchen und schnell fehlschlagen, anstatt Ressourcen zu verschwenden, indem andere Aufgaben abgeschlossen werden.
Sie sollten niemals die folgenden Aufgaben in DllMain ausführen:
- Rufen Sie LoadLibrary oder LoadLibraryEx auf (entweder direkt oder indirekt). Dies kann zu einem Deadlock oder abstürzen.
- Rufen Sie GetStringTypeA, GetStringTypeExoder GetStringTypeW (entweder direkt oder indirekt) auf. Dies kann zu einem Deadlock oder abstürzen.
- Synchronisieren mit anderen Threads. Dies kann zu einem Deadlock führen.
- Erwerben Sie ein Synchronisierungsobjekt, das sich im Besitz von Code befindet, der darauf wartet, die Ladesperre zu erhalten. Dies kann zu einem Deadlock führen.
- Initialisieren Sie COM-Threads mithilfe von CoInitializeEx. Unter bestimmten Bedingungen kann diese Funktion LoadLibraryEx aufrufen.
- Rufen Sie die Registrierungsfunktionen auf. Diese Funktionen werden in der Advapi32.dll. Wenn Advapi32.dll dll nicht initialisiert wird, kann die DLL auf nicht initialisierten Arbeitsspeicher zugreifen und den Prozess zum Absturz führen.
- Rufen Sie CreateProcess auf. Beim Erstellen eines Prozesses kann eine andere DLL geladen werden.
- Rufen Sie ExitThread auf. Das Beenden eines Threads während der DLL-Trennung kann dazu führen, dass die Ladesperre erneut aufgerufen wird, was zu einem Deadlock oder einem Absturz führt.
- Rufen Sie CreateThread auf. Das Erstellen eines Threads kann funktionieren, wenn Sie keine Synchronisierung mit anderen Threads ausführen, aber es ist riskant.
- Erstellen Sie eine Named Pipe oder ein anderes benanntes Objekt (nur Windows 2000). In Windows 2000 werden benannte Objekte von der Terminaldienste-DLL bereitgestellt. Wenn diese DLL nicht initialisiert ist, können Aufrufe der DLL zum Absturz des Prozesses führen.
- Verwenden Sie die Speicherverwaltungsfunktion aus dem dynamischen C Run-Time (CRT). Wenn die CRT-DLL nicht initialisiert ist, können Aufrufe dieser Funktionen zum Absturz des Prozesses führen.
- Aufrufen von Funktionen in User32.dll oder Gdi32.dll. Einige Funktionen laden eine andere DLL, die möglicherweise nicht initialisiert wird.
- Verwenden Sie verwalteten Code.
Die folgenden Aufgaben sind in DllMain sicher auszuführen:
- Initialisieren Sie statische Datenstrukturen und Member zur Kompilierzeit.
- Erstellen und Initialisieren von Synchronisierungsobjekten.
- Zuordnen von Arbeitsspeicher und Initialisieren dynamischer Datenstrukturen (Vermeiden der oben aufgeführten Funktionen)
- Einrichten des lokalen Threadspeichers (TLS).
- Öffnen, Lesen aus und Schreiben in Dateien.
- Rufen Sie Funktionen in Kernel32.dll auf (mit Ausnahme der oben aufgeführten Funktionen).
- Legen Sie globale Zeiger auf NULL fest, um die Initialisierung dynamischer Member zu deaktivieren. In Microsoft Windows Vista™ können Sie die einmaligen Initialisierungsfunktionen verwenden, um sicherzustellen, dass ein Codeblock nur einmal in einer Multithreadumgebung ausgeführt wird.
Deadlocks, die durch die Umkehrung der Sperr reihenfolge verursacht werden
Wenn Sie Code implementieren, der mehrere Synchronisierungsobjekte wie Sperren verwendet, ist es wichtig, die Sperr reihenfolge zu achten. Wenn es erforderlich ist, mehr als eine Sperre gleichzeitig zu erhalten, müssen Sie eine explizite Rangfolge definieren, die als Sperrhierarchie oder Sperrfolge bezeichnet wird. Wenn z. B. Sperre A vor Sperre B im Code und Sperre B an anderer Stelle im Code vor Sperre C erhalten wird, ist die Sperr reihenfolge A, B, C, und diese Reihenfolge sollte im gesamten Code befolgt werden. Die Umkehrung der Sperr reihenfolge tritt auf, wenn die Sperr reihenfolge nicht eingehalten wird, z. B. wenn Sperre B vor Sperre A übernommen wird. Die Umkehrung der Sperr reihenfolge kann Deadlocks verursachen, die schwer zu debuggen sind. Um solche Probleme zu vermeiden, müssen alle Threads Sperren in der gleichen Reihenfolge erhalten.
Beachten Sie, dass das Lader DllMain mit der bereits erworbenen Loadersperre aufruft, sodass die Ladersperre in der Sperrhierarchie die höchste Priorität haben sollte. Beachten Sie außerdem, dass Code nur die Sperren erhalten muss, die für eine ordnungsgemäße Synchronisierung erforderlich sind. Er muss nicht jede einzelne Sperre erhalten, die in der Hierarchie definiert ist. Wenn ein Codeabschnitt z. B. nur sperren muss, um A und C für eine ordnungsgemäße Synchronisierung zu sperren, sollte der Code Sperre A erhalten, bevor er Sperre C erhält. Es ist nicht erforderlich, dass der Code auch Sperre B erhält. Darüber hinaus kann dll-Code die Ladesperre nicht explizit erhalten. Wenn der Code eine API wie GetModuleFileName aufrufen muss, die indirekt die Ladesperre abrufen kann, und der Code auch eine private Sperre abrufen muss, sollte der Code GetModuleFileName aufrufen, bevor er die Sperre P abruft, um sicherzustellen, dass die Lade reihenfolge eingehalten wird.
Abbildung 2 ist ein Beispiel, das die Umkehrung der Sperr reihenfolge veranschaulicht. Stellen Sie sich eine DLL vor, deren Hauptthread DllMain enthält. Das Bibliotheklader ruft die Loadersperre L ab und ruft dann dllMain auf. Der Hauptthread erstellt die Synchronisierungsobjekte A, B und G, um den Zugriff auf seine Datenstrukturen zu serialisieren, und versucht dann, Sperren G zu erhalten. Ein Arbeitsthread, der die Sperre G bereits erfolgreich erhalten hat, ruft dann eine Funktion wie GetModuleHandle auf, die versucht, die Loadersperre L zu erhalten. Daher wird der Arbeitsthread auf L und der Hauptthread auf G blockiert, was zu einem Deadlock führt.

Um Deadlocks zu verhindern, die durch die Umkehrung der Sperr reihenfolge verursacht werden, sollten alle Threads jederzeit versuchen, Synchronisierungsobjekte in der definierten Lade reihenfolge zu erhalten.
Bewährte Methoden für die Synchronisierung
Stellen Sie sich eine DLL vor, die Arbeitsthreads als Teil der Initialisierung erstellt. Bei der DLL-Bereinigung ist es erforderlich, mit allen Arbeitsthreads zu synchronisieren, um sicherzustellen, dass sich die Datenstrukturen in einem konsistenten Zustand befinden, und dann die Arbeitsthreads zu beenden. Heutzutage gibt es keine einfache Möglichkeit, das Problem der sauberen Synchronisierung und des Herunterfahrens von DLLs in einer Multithreadumgebung vollständig zu lösen. In diesem Abschnitt werden die aktuellen bewährten Methoden für die Threadsynchronisierung beim Herunterfahren der DLL beschrieben.
Threadsynchronisierung in DllMain beim Beenden des Prozesses
- Wenn DllMain beim Beenden des Prozesses aufgerufen wird, wurden alle Threads des Prozesses erzürnt bereinigt, und es besteht die Möglichkeit, dass der Adressraum inkonsistent ist. In diesem Fall ist keine Synchronisierung erforderlich. Anders ausgedrückt: Der ideale DLL _ PROCESS _ DETACH-Handler ist leer.
- Windows Vista stellt sicher, dass sich Kerndatenstrukturen (Umgebungsvariablen, aktuelles Verzeichnis, Prozesshap und so weiter) in einem konsistenten Zustand befinden. Andere Datenstrukturen können jedoch beschädigt werden, sodass das Bereinigen des Speichers nicht sicher ist.
- Der persistente Zustand, der gespeichert werden muss, muss in den permanenten Speicher geleert werden.
Threadsynchronisierung in DllMain für DLL _ THREAD DETACH während des _ DLL-Entladens
- Wenn die DLL entladen wird, wird der Adressraum nicht verworfen. Daher wird erwartet, dass die DLL ein sauberes Herunterfahren ausführt. Dazu gehören Threadsynchronisierung, geöffnete Handles, persistenter Zustand und zugeordnete Ressourcen.
- Die Threadsynchronisierung ist schwierig, da das Warten auf das Beenden von Threads in DllMain zu einem Deadlock führen kann. Dll A enthält z. B. die Ladeprogrammsperre. Er signalisiert Thread T, dass er beendet wird, und wartet, bis der Thread beendet wird. Thread T wird beendet, und das Ladeprogramm versucht, die Ladeprogrammsperre zu erhalten, um dllMain von DLL A mit DLL _ THREAD DETACH aufzurufen. _ Dies führt zu einem Deadlock. So minimieren Sie das Risiko eines Deadlocks:
- DLL A ruft eine DLL _ THREAD _ DETACH-Nachricht in dllMain ab und legt ein Ereignis für Thread T fest und signalisiert, dass es beendet wird.
- Thread T beendet die aktuelle Aufgabe, versetzt sich in einen konsistenten Zustand, signalisiert DLL A und wartet unendlich. Beachten Sie, dass die Konsistenzprüfungsroutinen die gleichen Einschränkungen wie DllMain befolgen sollten, um Deadlocks zu vermeiden.
- DLL A beendet T mit dem Wissen, dass es sich in einem konsistenten Zustand befindet.
Wenn eine DLL entladen wird, nachdem alle threads erstellt wurden, aber bevor sie mit der Ausführung beginnen, können die Threads abstürzten. Wenn die DLL threads im Rahmen der Initialisierung in dllMain erstellt hat, haben einige Threads die Initialisierung möglicherweise noch nicht abgeschlossen, und ihre DLL _ THREAD _ ATTACH-Meldung wartet weiterhin darauf, an die DLL übermittelt zu werden. Wenn die DLL entladen wird, beginnt sie in diesem Fall mit dem Beenden von Threads. Einige Threads können jedoch hinter der Ladeprogrammsperre blockiert werden. Ihre DLL _ THREAD _ ATTACH-Meldungen werden verarbeitet, nachdem die DLL nicht zugeordnet wurde, wodurch der Prozess abstürzt.
Empfehlungen
Die folgenden Richtlinien werden empfohlen:
- Verwenden Sie Application Verifier, um die häufigsten Fehler in DllMainabzufangen.
- Wenn Sie eine private Sperre in DllMainverwenden, definieren Sie eine Sperrhierarchie, und verwenden Sie sie konsistent. Die Ladeprogrammsperre muss sich am unteren Rand dieser Hierarchie befinden.
- Vergewissern Sie sich, dass keine Aufrufe von einer anderen DLL abhängen, die möglicherweise noch nicht vollständig geladen wurde.
- Führen Sie einfache Initialisierungen statisch zur Kompilierzeit statt in DllMaindurch.
- Schieben Sie alle Aufrufe in DllMain zurück, die bis zu einem späteren Zeitpunkt warten können.
- Zurückstellen von Initialisierungstasks, die bis zu einem späteren Zeitpunkt warten können. Bestimmte Fehlerbedingungen müssen frühzeitig erkannt werden, damit die Anwendung Fehler ordnungsgemäß behandeln kann. Es gibt jedoch Kompromisse zwischen dieser frühen Erkennung und dem Verlust der Stabilität, die daraus resultieren können. Das Zurückstellen der Initialisierung ist häufig am besten geeignet.