Bewährte Methoden für die Dynamic Link Library
**Aktualisiert:**
- 17. Mai 2006
Wichtige APIs
Das Erstellen von DLLs stellt für Entwickler eine Reihe von Herausforderungen dar. DLLs weisen keine vom System erzwungene Versionsverwaltung auf. Wenn mehrere Versionen einer DLL auf einem System vorhanden sind, führt die Leichtigkeit des Überschreibens in Verbindung mit dem Fehlen eines Versionsschemas zu Abhängigkeits- und API-Konflikten. Komplexität in der Entwicklungsumgebung, die Ladeprogrammimplementierung und die DLL-Abhängigkeiten haben Schwachstellen in der Ladereihenfolge und im Anwendungsverhalten erzeugt. Schließlich stützen sich viele Anwendungen auf DLLs und haben komplexe Abhängigkeiten, die beachtet werden müssen, damit die Anwendungen ordnungsgemäß funktionieren. Dieses Dokument enthält Richtlinien für DLL-Entwickler, die bei der Erstellung robusterer, portabler und erweiterbarer DLLs helfen.
Falsche Synchronisierung innerhalb von DllMain kann dazu führen, dass eine Anwendung in einer nicht initialisierten DLL auf Daten oder Code zugreift. Das Aufrufen bestimmter Funktionen innerhalb von DllMain verursacht solche Probleme.
Allgemeine bewährte Methoden
DllMain wird aufgerufen, während die Ladeprogrammsperre gehalten wird. Daher gelten erhebliche Einschränkungen für die Funktionen, die innerhalb von DllMain aufgerufen werden können. DllMain ist daher so konzipiert, dass minimale Initialisierungsaufgaben mithilfe einer kleinen Teilmenge der Microsoft® Windows-API® ausgeführt werden. Sie können keine Funktion in DllMain aufrufen, die direkt oder indirekt versucht, die Ladeprogrammsperre zu übernehmen. Andernfalls besteht die Möglichkeit, dass Ihre Anwendung blockiert wird oder abstürzt. Ein Fehler in einer DllMain-Implementierung kann den gesamten Prozess und alle zugehörigen 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, die Initialisierung so weit wie möglich zu verschieben. Die verzögerte Initialisierung erhöht die Robustheit der Anwendung, da diese Initialisierung nicht durchgeführt wird, solange die Ladesperre gehalten wird. Außerdem können Sie dank der verzögerten Initialisierung viel mehr von der Windows-API verwenden.
Einige Initialisierungsaufgaben können nicht verschoben werden. Zum Beispiel sollte eine DLL, die von einer Konfigurationsdatei abhängt, nicht geladen werden, wenn die Datei fehlerhaft ist oder Müll enthält. Für diese Art der Initialisierung sollte die DLL versuchen, die Aktion auszuführen und schnell fehlschlagen, anstatt Ressourcen mit der Ausführung anderer Aufgaben zu verschwenden.
Sie sollten niemals die folgenden Aufgaben innerhalb von DllMain ausführen:
- LoadLibrary oder LoadLibraryEx (entweder direkt oder indirekt) aufrufen. Dies kann zu einem Deadlock oder einem Absturz führen.
- GetStringTypeA, GetStringTypeEx oder GetStringTypeW (entweder direkt oder indirekt) aufrufen. Dies kann zu einem Deadlock oder einem Absturz führen.
- Synchronisieren mit anderen Threads. Dies kann zu einem Deadlock führen.
- Erwerben Sie ein Synchronisierungsobjekt, das im Besitz von Code ist, der darauf wartet, die Ladeprogrammsperre zu übernehmen. 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.
- Rufen Sie CreateProcess auf. Das Erstellen eines Prozesses kann eine andere DLL laden.
- Rufen Sie ExitThread auf. Das Beenden eines Threads während der DLL-Trennung kann dazu führen, dass die Ladeprogrammsperre erneut abgerufen wird, was zu einem Deadlock oder einem Absturz führt.
- Rufen Sie CreateThread auf. Das Erstellen eines Threads kann funktionieren, wenn Sie nicht mit anderen Threads synchronisieren, aber es ist riskant.
- Rufen Sie ShGetFolderPathW auf. Das Aufrufen von Shell-/bekannten Ordner-APIs kann zu Threadsynchronisierung führen und kann daher Deadlocks verursachen.
- Erstellen Sie ein 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 dazu führen, dass der Prozess abstürzt.
- Verwenden Sie die Speicherverwaltungsfunktion aus der dynamischen C-Laufzeit (C Run-Time, CRT). Wenn die CRT-DLL nicht initialisiert ist, können Aufrufe dieser Funktionen dazu führen, dass der Prozess abstürzt.
- Rufen Sie Funktionen in User32.dll oder Gdi32.dll auf. Einige Funktionen laden eine andere DLL, die möglicherweise nicht initialisiert wird.
- Verwenden Sie verwalteten Code.
Die folgenden Aufgaben können innerhalb von DllMain sicher ausgeführt werden:
- Initialisieren Sie statische Datenstrukturen und Mitglieder zur Kompilierungszeit.
- Erstellen und initialisieren Sie Synchronisierungsobjekte.
- Weisen Sie Arbeitsspeicher zu und initialisieren Sie dynamische Datenstrukturen (vermeiden Sie die oben aufgeführten Funktionen.)
- Richten Sie den lokalen Thread-Speicher (TLS) ein.
- Öffnen, Lesen 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, wodurch die Initialisierung dynamischer Elemente aufgehoben wird. 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 Inversion der Sperrreihenfolge verursacht werden
Wenn Sie Code implementieren, der mehrere Synchronisierungsobjekte wie Sperren verwendet, ist es wichtig, die Sperrreihenfolge zu respektieren. Wenn es notwendig ist, mehr als eine Sperre auf einmal zu erhalten, müssen Sie eine explizite Rangfolge festlegen, die als Sperrhierarchie oder Sperrreihenfolge bezeichnet wird. Wenn beispielsweise die Sperre A vor der Sperre B irgendwo im Code erworben wird und die Sperre B vor der Sperre C an einer anderen Stelle im Code, dann ist die Reihenfolge der Sperren A, B, C und diese Reihenfolge sollte im gesamten Code eingehalten werden. Die Umkehrung der Sperrreihenfolge tritt auf, wenn die Sperrreihenfolge nicht eingehalten wird, z. B. wenn Sperre B vor Sperre A erworben wird. Die Umkehrung der Sperrreihenfolge kann zu Deadlocks führen, die schwer zu debuggen sind. Um solche Probleme zu vermeiden, müssen alle Threads in der gleichen Reihenfolge Sperren erwerben.
Es ist wichtig zu beachten, dass das Ladeprogramm DllMain mit der bereits erworbenen Ladesperre aufruft, sodass die Ladeprogrammsperre die höchste Priorität in der Sperrhierarchie haben sollte. Beachten Sie auch, dass der Code nur die Sperren erwerben muss, die er für eine ordnungsgemäße Synchronisierung benötigt. Er muss nicht jede einzelne Sperre erwerben, die in der Hierarchie definiert ist. Wenn beispielsweise ein Codeabschnitt nur A und C für die ordnungsgemäße Synchronisierung sperrt, sollte der Code die Sperre A abrufen, bevor er die C-Sperre erwirbt. Es ist nicht erforderlich, dass der Code auch die Sperre B erwirbt. Außerdem kann DLL-Code nicht explizit die Ladesperre erwerben. Wenn der Code eine API wie GetModuleFileName aufrufen muss, die indirekt die Ladeprogrammsperre erwerben kann, und der Code auch eine private Sperre erwerben muss, dann sollte der Code GetModuleFileName aufrufen, bevor er die Sperre P erwirbt, um so sicherzustellen, dass die Reihenfolge des Ladens eingehalten wird.
Abbildung 2 ist ein Beispiel, das die Umkehrung der Sperrreihenfolge veranschaulicht. Erwägen Sie eine DLL, deren Hauptthread DllMain enthält. Das Bibliotheksladeprogramm erwirbt die Ladeprogrammsperre L und ruft dann DllMain auf. Der Hauptthread erstellt Synchronisierungsobjekte A, B und G, um den Zugriff auf seine Datenstrukturen zu serialisieren und versucht dann, die Sperre G abzurufen. Ein Arbeitsthread, der die Sperre G bereits erfolgreich erworben hat, ruft dann eine Funktion wie GetModuleHandle auf, die versucht, die Ladeprogrammsperre L abzurufen. Daher wird der Arbeitsthread auf L blockiert, und der Hauptthread wird auf G blockiert, was zu einem Deadlock führt.
Um Deadlocks zu vermeiden, die durch die Umkehrung der Sperrreihenfolge verursacht werden, sollten alle Threads versuchen, Synchronisationsobjekte immer in der definierten Ladereihenfolge zu erwerben.
Bewährte Methoden für die Synchronisierung
Erwägen Sie eine DLL, 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 beenden. Derzeit gibt es keine einfache Möglichkeit, das Problem der sauberen Synchronisierung und Herunterfahren 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 zwangsweise bereinigt und es besteht die Möglichkeit, dass der Adressraum inkonsistent ist. Die Synchronisierung ist in diesem Fall nicht erforderlich. Mit anderen Worten, der ideale DLL_PROCESS_DETACH Handler ist leer.
- Windows Vista stellt sicher, dass die Kerndatenstrukturen (Umgebungsvariablen, aktuelles Verzeichnis, Prozess-Heap usw.) in einem konsistenten Zustand sind. Andere Datenstrukturen können jedoch beschädigt werden, sodass eine Speicherbereinigung nicht sicher ist.
- Der permanente Zustand, der gespeichert werden muss, muss auf dauerhaften 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 entfernt. Daher wird erwartet, dass die DLL ein sauberes Herunterfahren durchführt. Dazu gehören die Synchronisierung von Threads, offene Handles, dauerhafte Zustände und zugewiesene Ressourcen.
- Die Synchronisierung von Threads ist schwierig, da das Warten auf das Beenden von Threads in DllMain zu einem Deadlock führen kann. Beispielsweise enthält DLL A die Ladesperre. Es signalisiert, Thread T zu beenden und wartet, bis der Thread beendet wird. Thread T wird beendet und das Ladeprogramm versucht, die Ladesperre zu erhalten, um DLL A's DllMain 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 der DllMain ab und legt ein Ereignis für Thread T fest, was signalisiert, dass sie beendet wird.
- Thread T beendet seine aktuelle Aufgabe, bringt sich in einen konsistenten Zustand, signalisiert DLL A und wartet unbegrenzt. Beachten Sie, dass die Routinen zur Konsistenzprüfung denselben Einschränkungen unterliegen sollten wie DllMain, um Deadlocks zu vermeiden.
- DLL A beendet T in 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ürzen. Wenn die DLL im Rahmen ihrer Initialisierung Threads in ihrer DllMain erstellt hat, kann es sein, dass einige Threads ihre Initialisierung noch nicht abgeschlossen haben und ihre DLL_THREAD_ATTACH-Nachricht noch darauf wartet, an die DLL übermittelt zu werden. Wenn die DLL in diesem Fall entladen wird, beginnt sie mit dem Beenden von Threads. Einige Threads können jedoch hinter der Ladeprogrammsperre blockiert werden. Die DLL_THREAD_ATTACH Nachrichten werden verarbeitet, nachdem die DLL nicht zugeordnet wurde, wodurch der Prozess abstürzt.
Empfehlungen
Nachfolgend sind die empfohlenen Richtlinien aufgeführt:
- Verwenden Sie Application Verifier, um die häufigsten Fehler in DllMain abzufangen.
- Wenn Sie eine private Sperre innerhalb von DllMain verwenden, definieren Sie eine Sperrhierarchie und verwenden Sie diese konsistent. Die Ladesperre muss in dieser Hierarchie ganz unten stehen.
- Stellen Sie sicher, dass keine Aufrufe von einer anderen DLL abhängen, die möglicherweise noch nicht vollständig geladen wurde.
- Führen Sie einfache Initialisierungen zur Kompilierungszeit statisch statt in DllMain durch.
- Verschieben Sie alle Aufrufe in DllMain, die auf einen späteren Zeitpunkt warten können.
- Verschieben Sie Initialisierungsaufgaben, die auf einen 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, der sich daraus ergeben kann. Das Zurückstellen der Initialisierung ist häufig am besten geeignet.