DllMain und die Ladersperre — Der wahre Grund, warum man Ihnen sagt, „in der DLL-Initialisierung nichts zu tun“

· · Windows, DLL, Windows-Entwicklung, C++, Fehleruntersuchung, Multithreading, Win32-API

„Die App hängt beim Start, aber nur in einer bestimmten Umgebung.“ „Wenn wir unsere eigene DLL laden, kehrt LoadLibrary manchmal nie zurück.“ „Es deadlockt nur zum Zeitpunkt eines Dienststarts.“ — Verfolgen Sie Untersuchungen wie diese weit genug, und Sie kommen häufiger als nicht am selben Ort an. Dem Initialisierungscode der DLL — das heißt DllMain.

Die Dokumentation von Microsoft warnt über DllMain in einem ungewöhnlich scharfen Ton. Rufen Sie LoadLibrary nicht auf. Synchronisieren Sie nicht mit anderen Threads. Rufen Sie keine User-, Shell- oder COM-Funktionen auf. Die ideale DllMain ist ein leerer Stub — warum ist die Sprache so scharf? Der Grund konzentriert sich in einem einzigen internen Mechanismus, der Ladersperre (Loader Lock). An Entwickler gerichtet, die DLLs, Plug-ins und C++/CLI-Wrapper unter Windows schreiben, erklärt dieser Artikel anhand von Primärquellen, wie die Ladersperre funktioniert, die Struktur, die einen Deadlock hält, und den sicheren Entwurf sowie das Untersuchungsverfahren.

1. Zuerst das Fazit

  • DllMain wird aufgerufen, während die Ladersperre gehalten wird, eine gemeinsame Sperre, von der es genau eine pro Prozess gibt. Also schafft das Aufrufen von Arbeit aus DllMain, die versucht, die Ladersperre zu nehmen (direkt oder indirekt), die Möglichkeit eines Deadlocks oder eines Absturzes durch das Anfassen einer DLL, die noch nicht initialisiert ist.1
  • Das Aufrufen von LoadLibrary / FreeLibrary ist verboten. Es erzeugt eine zirkuläre Abhängigkeit der Ladereihenfolge und kann dazu führen, dass Initialisierungscode gegen eine DLL läuft, deren eigene Initialisierung noch nicht gelaufen ist.2
  • Das Synchronisieren mit anderen Threads ist ebenfalls verboten. DLL-Benachrichtigungen sind serialisiert, also lässt das Warten innerhalb von DllMain auf den Start oder das Ende eines Threads diesen Thread selbst stehen, der auf die Ladersperre wartet, und Sie deadlocken.23
  • Was Sie sicher aufrufen können, ist in der Praxis nur eine Teilmenge von Kernel32.dll. Und die offizielle Dokumentation sagt deutlich, dass „eine vollständige Liste sicherer Funktionen nicht existiert“. User-, Shell- und COM-Funktionen laden andere Komponenten und verursachen Zugriffsverletzungen.2
  • In einer mit der CRT gebundenen DLL gelten dieselben Einschränkungen für Konstruktoren und Destruktoren von Globalen. Sie laufen als de-facto-Teil von DllMain.2
  • Der richtige Entwurf ist „aufschieben“. Tun Sie die Initialisierung, die Sie können, zur Kompilierzeit (statisch); schieben Sie das, was Sie nicht können, bis zur ersten Nutzung auf. Das ist die offizielle Best Practice.1
  • Gemischte C++/CLI-DLLs sind besonders gefährlich. Um zu vermeiden, MSIL unter der Ladersperre auszuführen, müssen DllMain und sein Aufrufbaum nativ kompiliert werden.4

2. Wann und wie DllMain aufgerufen wird

DllMain ist der Einstiegspunkt, den der OS-Lader aufruft, wenn eine DLL einen Prozess oder einen Thread betritt oder verlässt. Es gibt vier Benachrichtigungen.

Benachrichtigung Zeitpunkt
DLL_PROCESS_ATTACH Wenn die DLL in den Prozess geladen wird
DLL_THREAD_ATTACH Wenn ein neuer Thread im Prozess gestartet wird
DLL_THREAD_DETACH Wenn ein Thread normal endet
DLL_PROCESS_DETACH Wenn die DLL entladen wird oder der Prozess endet

Zwei Tatsachen sind leicht zu übersehen. Erstens: Jedes Mal, wenn ein einzelner Thread angelegt wird, wird DllMain jeder bereits geladenen DLL mit DLL_THREAD_ATTACH aufgerufen. Mit anderen Worten ist DllMain nicht „etwas, das einmal läuft, wenn meine DLL geladen wird“; es ist Code, der für die Thread-Aktivität des Prozesses weiter aufgerufen wird. Wenn Sie das nicht brauchen, können Sie es stoppen, indem Sie DisableThreadLibraryCalls innerhalb von DLL_PROCESS_ATTACH aufrufen (rufen Sie es nicht aus einer mit der statischen CRT gebundenen DLL auf).5

Zweitens: In einer mit der CRT (der C/C++-Laufzeit) gebundenen DLL laufen Konstruktoren und Destruktoren von globalen und statischen C++-Objekten über den Einstiegspunkt der CRT als Teil von DllMain.2 Selbst wenn Sie denken „unsere DllMain ist leer, also sind wir sicher“, ist ein globales Objekt mit aufwendiger Initialisierung dasselbe, als liefe diese Arbeit in DllMain.

Die vier Zeitpunkte, zu denen DllMain aufgerufen wirdDLL_PROCESS_ATTACH läuft beim DLL-Laden; DLL_THREAD_ATTACH und DETACH laufen auf jeder bereits geladenen DLL bei jedem Thread-Start und -Ende im Prozess; DLL_PROCESS_DETACH läuft beim Entladen oder Prozessende; und Konstruktoren statischer Objekte laufen hier ebenfalls über die CRTDLL-LadenDLL_PROCESS_ATTACHDLL_THREAD_ATTACH(bei jedem Thread-Start)DLL_THREAD_DETACH(bei jedem Thread-Ende)DLL_PROCESS_DETACH(beim Entladen oder Ende)Konstruktion statischer Objekte läuft hier ebenfalls

Abbildung 1: DllMain wird nicht nur zur Ladezeit, sondern bei jedem Thread-Start und -Ende aufgerufen, und die Initialisierung statischer Objekte läuft ebenfalls als Teil davon.

3. Die Ladersperre — Eine Sperre, die jede Benachrichtigung serialisiert

Warum sind die Einschränkungen allein bei DllMain so streng? Die Antwort liegt in der Struktur des Laders.

Um eine Reihe von Operationen — DLL-Laden, Entladen und die verschiedenen Benachrichtigungen — konsistent zu halten, serialisiert der OS-Lader die Arbeit mit einer einzigen Ladersperre pro Prozess. Und der wichtige Punkt ist, dass DllMain aufgerufen wird, während diese Ladersperre gehalten wird.1 Solange Sie in DllMain sind, wartet jedes andere DLL-Laden in diesem Prozess und jede Thread-Start-Benachrichtigung auf die Freigabe dieser Sperre.

Aus dieser Struktur folgen die Gründe für die Verbote nacheinander.

  • Sie dürfen LoadLibrary nicht aufrufen, weil das Wiedereintreten in die Ladersperre oder eine zirkuläre Abhängigkeit der Ladereihenfolge erzeugt. Es kann auch dazu führen, eine Funktion auf einer DLL aufzurufen, deren Initialisierung noch nicht fertig ist.2
  • Das Synchronisieren mit anderen Threads ist gefährlich, weil der Thread, auf den Sie warten, Momente hat, in denen er die Ladersperre braucht (Benachrichtigungen bei Start und Ende, Aufrufe von APIs der GetModuleHandle-Familie und so weiter). Sie halten die Ladersperre und warten auf die Gegenseite; die Gegenseite wartet auf die Ladersperre — die klassische Umkehrung der Sperrreihenfolge.6
  • User-, Shell- und COM-Funktionen sind gefährlich, weil sie intern andere Systemkomponenten laden. Sie fassen eine Komponente an, bevor sie initialisiert ist oder nachdem sie abgebaut wurde, und Sie bekommen eine Zugriffsverletzung.2
Warum das Warten auf einen Thread innerhalb von DllMain deadlocktDllMain, die die Ladersperre hält, wartet auf das Ende eines Worker-Threads, aber der Worker, der zu enden versucht, wartet auf die Freigabe der Ladersperre, um DLL_THREAD_DETACH zu empfangen, sodass sie aufeinander warten und deadlockenWorkerDllMainLader(hält Sperre)WorkerDllMainLader(hält Sperre)Die Endbenachrichtigung braucht die SperreDllMain hält die Sperre, W wartetDLL_PROCESS_DETACHEnde anfordern und wartenArbeit beenden, dann enden

Abbildung 2: „DllMain wartet auf das Ende eines Threads“ ist ein struktureller Deadlock, weil das Thread-Ende selbst die Ladersperre braucht.

Der Punkt ist, dass dies nicht die Art von Sache ist, die „passiert, wenn Sie Pech haben“; es ist strukturell garantiert, dass es hält. Die Dokumentation sagt Ihnen, die Ladersperre als die Spitze der Sperrhierarchie zu behandeln, die die App definiert (die zuerst genommene). Innerhalb von DllMain halten Sie diese oberste Sperre bereits, also ist jede Handlung, von dort aus auf etwas anderes zu warten, gefährlich — das ist eine nützliche Art, es zu merken.6

Umkehrung der Sperrreihenfolge zwischen Ladersperre und privater SperreDllMain, die die Ladersperre hält, geht, um eine private Sperre zu nehmen, während ein Worker, der diese private Sperre hält, geht, um die Ladersperre für GetModuleHandle oder Ähnliches zu nehmen, sodass die Erwerbsreihenfolge umkehrt und sie deadlockenDllMain: hält die LadersperreGeht, um private Sperre G zu nehmenWorker: hält private Sperre GGeht, um die Ladersperre zu nehmenDeadlock durch umgekehrte ErwerbsreihenfolgeGetModuleHandle und Ähnliches brauchen sie intern

Abbildung 3: Sogar eine harmlose API wie GetModuleHandle braucht intern die Ladersperre, sodass eine Reihenfolgeumkehrung mit einer privaten Sperre halten kann.

Außerdem ist das Aufrufen von CreateThread aus DllMain selbst nicht empfohlen. Der angelegte Thread braucht die Ladersperre, um die DLL_THREAD_ATTACH-Benachrichtigung zu verarbeiten, also kann er nicht zu laufen beginnen, bis die gerade ausführende DllMain zurückkehrt und die Sperre freigibt. Daher ist das Warten innerhalb von DllMain auf den Start oder das Ende dieses Threads ein sofortiger Deadlock. Es gibt auch ein Lebensdauerproblem — wenn nach der Rückkehr von DllMain die DLL entladen wird, während ein Thread, der noch nicht zu laufen begonnen hat, noch zurückbleibt, zeigt die Startadresse des Threads noch auf bereits freigegebenen Code und Sie stürzen ab.3

4. Zwei Minen, auf die C++-Entwickler leicht treten

Mine 1: Dynamische Initialisierung globaler Objekte. Wie Kapitel 2 sagte, laufen Konstruktoren statischer Objekte unter den DllMain-Einschränkungen. Eine Konfigurationsdatei lesen, eine Protokollierungseinrichtung aufstellen, COM initialisieren, einen Thread starten — in dem Moment, in dem Sie ein Global in eine DLL setzen, dessen Konstruktor diese Art von Arbeit tut, führen Sie „Dinge aus, die Sie in DllMain nicht tun dürfen“. Konstante Initialisierung, die zur Kompilierzeit festliegt (alles, was Sie constexpr machen können), ist sicher; Initialisierung, die einen Funktionsaufruf umfasst, sollte aufgeschoben werden.

Der Pfad, auf dem das Initialisieren eines globalen Objekts zur Mine wirdDie Ladersperre wird beim DLL-Laden genommen, und Konstruktoren globaler Objekte laufen über den CRT-Einstiegspunkt, sodass LoadLibrary, Thread-Synchronisierung und COM-Initialisierung innerhalb dieser Konstruktoren Ausführungen von DllMain-Verboten sindDLL-Laden(Ladersperre erworben)CRT-EinstiegspunktKonstruktor eines globalen ObjektsLoadLibrary-äquivalente ArbeitEinen Thread starten und auf sein Ende wartenCOM oder User32 nutzenAll das fällt unter DllMain-Verbote

Abbildung 4: Selbst „DllMain ist leer, also sind wir sicher“ belebt dieselbe Gefahr in dem Moment wieder, in dem Sie ein Global mit aufwendiger Initialisierung haben.

Mine 2: C++/CLI (gemischte Assemblies). In einer Konfiguration, die eine native DLL mit C++/CLI umhüllt (die Form, die im Wrapper-Artikel behandelt ist), besteht die Gefahr, MSIL (verwalteten Code) unter der Ladersperre auszuführen. Das Ausführen von MSIL kann die CLR-Initialisierung oder das Laden einer anderen Assembly auslösen. Der Compiler gibt die Warnung C4747 auf Code aus, in dem DllMain MSIL direkt ausführt, aber er kann indirekte Ausführung über eine Funktion in einem anderen Modul nicht erkennen. Kompilieren Sie DllMain und die von ihr aufgerufenen Funktionen mit #pragma unmanaged nativ, oder nutzen Sie eine Konfiguration, die gar keine DllMain hat.4

Ob MSIL-Ausführung unter der Ladersperre erkannt werden kannCode, in dem DllMain MSIL direkt ausführt, kann der Compiler mit der Warnung C4747 erkennen, aber indirekte Ausführung über eine Funktion in einem anderen Modul nicht, sodass Sie sie durch Prüfung des Aufrufbaums und Bestehen auf native Kompilierung verhindern müssenAufrufe aus DllMainMSIL direkt ausführenÜber ein anderes Modul ausführenErkennbar mit Warnung C4747Der Compiler kann es nicht erkennenMit Prüfung und #pragma unmanaged verhindern

Abbildung 5: C4747 schützt Sie nur gegen direkte Ausführung. Indirekte Pfade können nur durch Prüfung gefangen werden.

5. Der richtige Entwurf — „Aufschieben“ zur Standardpolitik machen

Die offizielle Best-Practice-Empfehlung ist klar.1

  1. Beenden Sie die Initialisierung, die Sie können, zur Kompilierzeit (statisch). Fragen Sie zuerst, ob eine dynamische Initialisierung durch eine statische ersetzt werden kann.
  2. Schieben Sie den Rest bis zur ersten Nutzung auf. Solange die erste Nutzung von einer gewöhnlichen API kommt, die aufgerufen wird, nachdem die DLL fertig geladen ist, läuft die Initialisierung außerhalb der Ladersperre und Sie können sicher fast die ganze Windows-API nutzen. Für den Ausschluss beim ersten Zugriff können Sie INIT_ONCE (Einmalinitialisierung) oder C++ magic statics (funktionslokale statics) nutzen. Aufschieben ist allerdings kein Allheilmittel — wenn dieser erste Zugriff selbst aus DllMain oder einem statischen Initialisierer kommt, läuft der Initialisierer immer noch unter der Ladersperre und Sie sind wieder unter denselben Einschränkungen.
  3. Machen Sie eine Ausnahme nur für Fehlschläge, die Sie früh erkennen müssen. Sie können die Anforderung haben, dass eine kaputte Konfigurationsdatei das Laden selbst fehlschlagen lassen soll. Auch dann halten Sie es auf das Minimum von „versuchen und sofort fehlschlagen“.
  4. Erwägen Sie DisableThreadLibraryCalls in DLL_PROCESS_ATTACH. Wenn die DLL Thread-Benachrichtigungen nicht nutzt, können Sie die Benachrichtigungskosten selbst entfernen (außer bei Nutzung der statischen CRT oder statischem TLS).5
  5. Prüfen Sie mit Application Verifier. Viele der gefährlichen Aufrufe innerhalb von DllMain sind solche, die Application Verifier zur Laufzeit erkennt.1
Entwurfsleitlinie für die DLL-InitialisierungErwägen Sie zuerst, ob Initialisierung kompilierzeit-statische Initialisierung sein kann; wenn nicht, ist die Vorgabe, bis zur ersten Nutzung aufzuschieben, und in DllMain nur das Minimum zu lassen, das früh als Ladefehler erkannt werden mussjaneinneinjaKann es zur Kompilierzeit entschieden werden?Machen Sie es zur statischen InitialisierungMuss der Fehlschlag zur Ladezeit erkannt werden?Bis zur ersten Nutzung aufschieben(die Vorgabe)Nur das Minimum in DllMain tunMit INIT_ONCE oder einer funktionslokalen static ausschließen

Abbildung 6: Die Entscheidungsreihenfolge ist „kann es statisch sein → kann es aufgeschoben werden“, und was Sie in DllMain lassen, ist nur das Minimum, das früh erkannt werden muss.

Ob Sie DisableThreadLibraryCalls anwenden, können Sie mit der folgenden Verzweigung mechanisch entscheiden.

Ob DisableThreadLibraryCalls aufgerufen werden sollRufen Sie es nicht aus einer mit der statischen CRT gebundenen DLL auf; wenn statisches TLS in Kraft ist, schlägt der Aufruf selbst fehl, also rufen Sie es nicht auf; gilt keines von beiden und die DLL nutzt keine Thread-Benachrichtigungen, rufen Sie es in DLL_PROCESS_ATTACH auf und prüfen den Rückgabewert, um die Benachrichtigungskosten zu senkenjaneinjaneinneinjaMit der statischen CRT gebunden?Darf nicht aufgerufen werdenStatisches TLS in Nutzung?Der Aufruf schlägt sowieso fehl(FALSE)Werden Thread-Benachrichtigungen gebraucht?In ATTACH aufrufen(Rückgabewert prüfen)Nicht aufrufen; die Benachrichtigungen behandeln

Abbildung 7: Die drei Bedingungen statische CRT, statisches TLS und ob Benachrichtigungen gebraucht werden, entscheiden eindeutig, ob Sie es aufrufen sollten.

Für das Stoppen von Threads beim Entladen gibt die offizielle Dokumentation ein konkretes Protokoll. Statt in DLL_PROCESS_DETACH (bei einem Entladen über FreeLibrary) auf das Ende von Worker-Threads zu „warten“, ist die Form (1) Ende mit einem Ereignis signalisieren, (2) die Thread-Seite faltet ihre Arbeit auf einen konsistenten Zustand zusammen, signalisiert zurück und geht in ein unendliches Warten, (3) die DllMain-Seite bestätigt den konsistenten Zustand und faltet den Thread dann mit TerminateThread.3 Es sieht grob aus, aber es ist als die realistische Antwort innerhalb der Einschränkung „Sie dürfen innerhalb von DllMain nicht auf das natürliche Ende eines Threads warten“ dokumentiert.

Protokoll zum Stoppen eines Threads beim EntladenDllMain signalisiert dem Worker-Thread mit einem Ereignis das Ende; der Worker faltet seine Arbeit auf einen konsistenten Zustand zusammen, signalisiert zurück und geht in ein unendliches Warten; DllMain bestätigt den konsistenten Zustand und beendet den Thread dannWorker-ThreadDllMain(DETACH-Behandlung)Worker-ThreadDllMain(DETACH-Behandlung)Kein Warten auf natürliches Ende, also kein DeadlockEnde mit einem Ereignis signalisierenArbeit auf einen konsistenten Zustand faltenKonsistenz fertig signalisieren und ewig wartenMit TerminateThread beenden

Abbildung 8: Statt „auf ein natürliches Ende warten“ vermeidet „auf ein Konsistenzsignal warten und dann abschneiden“ eine Kollision mit der Ladersperre.

Als Sache erster Prinzipien ist der sicherste Entwurf, das Besitzen von Threads in einer DLL zu vermeiden, die entladen werden kann, und den Thread-Besitz auf der EXE-Seite zu halten.

DLL_PROCESS_DETACH am Prozessende ist das Gegenteil: nichts tun und zurückkehren ist das Ideal. Zu diesem Zeitpunkt ist jeder andere Thread bereits zwangsweise beendet, und Sie können sich auch nicht auf den Zustand abhängiger DLLs oder der Laufzeit verlassen. Aufwendige Arbeit hier verursacht nur Deadlocks und Abstürze. Daten, die erhalten bleiben müssen, sollten im eigenen Beendigungspfad der App herausgeschrieben werden; verlassen Sie sich nicht auf diese Benachrichtigung.3

6. Wie Sie untersuchen, wenn Sie hineinlaufen

Hänger der Ladersperre haben einen erkennbaren Fingerabdruck.

Schauen Sie sich die Stacks in einem Hänger-Dump an. Nehmen Sie einen Dump des eingefrorenen Moments und prüfen Sie den Stack jedes Threads. Wenn Sie ein Paar finden aus einem Thread, der auf eine Sperre innerhalb von Laderfunktionen von ntdll.dll wartet (die Familie, deren Namen mit Ldr beginnen), und einem Thread, der innerhalb von DllMain oder einem statischen Initialisierer (dynamic initializer) auf etwas anderes wartet, sind Sie fast sicher. Ein Thread, der mitten in einem LoadLibrary-Aufruf steht, ist ein weiterer typischer Charakter.

Der Fingerabdruck eines Ladersperren-HängersWenn Sie in einem Hänger-Dump sowohl einen Thread finden, der auf eine Sperre innerhalb von ntdll-Laderfunktionen wartet, als auch einen Thread, der innerhalb von DllMain oder einem statischen Initialisierer auf etwas anderes wartet, können Sie es mit nahezu Sicherheit als Ladersperren-Deadlock behandelnjaneinHänger-DumpThread wartet auf eine Sperre in Ldr-FunktionenThread wartet innerhalb von DllMain oder einem statischen InitialisiererBeide vorhanden?Fast sicher ein Ladersperren-DeadlockAls Hänger anderer Art untersuchen

Abbildung 9: Ladersperren-Hänger haben den erkennbaren Fingerabdruck „Warten in Ldr + Warten innerhalb von DllMain“.

Verdächtigen Sie den „zeitabhängigen“ Charakter. Ein Ladersperren-Deadlock hält nur in dem Moment, in dem ein DLL-Laden mit einem Thread-Start oder -Ende zusammenfällt. Reproduktionsbedingungen wie „gelegentlich beim Start“, „nur auf einer bestimmten Maschine“ und „nur wenn als Dienst ausgeführt“ sind Zeichen dieser Art von Problem.

Führen Sie vorbeugende Prüfung aus. Aktivieren Sie Application Verifier und führen Sie Ihre Tests aus, und Sie können gefährliche Aufrufe innerhalb von DllMain zur Laufzeit erkennen.1 Für C++/CLI ignorieren Sie die Warnung C4747 nicht; in Prüfungen von Funktionen, die von DllMain erreichbar sind, fügen Sie den Blickwinkel „Funktionen, die LoadLibrary indirekt aufrufen“ (COM-Initialisierung, einige CRT-Funktionen, verzögert geladene Importe und so weiter) zur Prüfliste hinzu, und Sie fangen Unfälle, bevor Sie sie ausliefern. Der erste Aufruf eines verzögert geladenen Imports, der intern zu LoadLibrary wird, ist ein leicht zu übersehender Punkt.

7. Zusammenfassung

  • DllMain wird aufgerufen, während die Ladersperre gehalten wird (eine pro Prozess, die Sperre, die jede DLL-Benachrichtigung serialisiert). Jede Einschränkung folgt daraus.
  • Der Kern der Verbote ist „rufen Sie LoadLibrary / FreeLibrary nicht auf“, „synchronisieren Sie nicht mit anderen Threads“ und „rufen Sie keine Funktionen auf, die von einer anderen DLL als Kernel32 abhängen“. Konstruktoren und Destruktoren statischer Objekte, die über die CRT laufen, fallen unter dieselben Einschränkungen.
  • Die grundlegende Entwurfspolitik ist Aufschieben. Machen Sie die Initialisierung statisch, die Sie statisch machen können; schieben Sie den Rest bis zur ersten Nutzung auf. Nutzen Sie DisableThreadLibraryCalls und Application Verifier.
  • Das Stoppen von Threads beim Entladen folgt dem offiziellen Protokoll (signalisieren → Konsistenz bestätigen → beenden). DLL_PROCESS_DETACH am Prozessende ist idealerweise leer.
  • In C++/CLI ist das Ausführen von MSIL unter der Ladersperre eine Mine für sich. Bestehen Sie auf eine native Kompilierung des DllMain-Aufrufbaums.

Die DllMain-Einschränkungen sehen zunächst wie eine unvernünftige Verbotsliste aus. Aber sobald Sie den einen Punkt halten, dass „sie aufgerufen wird, während die Ladersperre, die oberste Sperre, gehalten wird“, ist jedes Verbot eine Umformulierung desselben Prinzips. Merken Sie es als Prinzip, und wenn Sie einen Randfall treffen, der nicht in der Dokumentation steht, sollten Sie immer noch die richtige Frage stellen können: „Ist das Arbeit, die ich tun darf, während ich die Sperre halte?“

Weiterführende Artikel

Zugehörige Beratungsfelder

KomuraSoft LLC übernimmt Ursachenuntersuchung von Hängern und Deadlocks zur Start- oder DLL-Ladezeit (Dump-Analyse), Entwurfsreviews rund um DllMain und statische Initialisierung sowie die Sanierung von C++/CLI-Wrappern und Plug-in-DLLs hin zu einem sicheren Initialisierungsentwurf. Sie können uns auch in der schwer reproduzierbaren Stufe „es hängt nur in einer bestimmten Umgebung beim Start“ konsultieren.

Quellen

  1. Microsoft Learn, Dynamic-Link Library Best Practices. Dazu, dass DllMain aufgerufen wird, während die Ladersperre gehalten wird, sodass die Funktionen, die Sie aufrufen können, stark eingeschränkt sind; dazu, dass die ideale DllMain ein leerer Stub ist und Initialisierung so weit wie möglich aufgeschoben wird; zur Empfehlung kompilierzeit-statischer Initialisierung; dazu, nur das Minimum für Fehlschläge zu tun, die früh erkannt werden müssen; sowie zum Erkennen typischer DllMain-Fehler mit Application Verifier.  2 3 4 5 6

  2. Microsoft Learn, DllMain entry point. Dazu, im Einstiegspunkt nur einfache Initialisierung und Beendigung auszuführen; warum Sie LoadLibrary / FreeLibrary nicht aufrufen dürfen (zirkuläre Ladereihenfolge und Nutzung einer DLL vor der Initialisierung oder nach der Beendigung); dazu, dass Kernel32.dll garantiert bereits geladen ist, sodass Sie sie in dem Bereich aufrufen können, der keine anderen DLLs lädt; dazu, dass es keine erschöpfende Liste sicherer Funktionen gibt; dazu, dass User-, Shell- und COM-Funktionen Zugriffsverletzungen verursachen; dazu, dass DLL-Benachrichtigungen serialisiert sind, sodass Kommunikation mit anderen Threads oder Prozessen Deadlocks verursacht; sowie dazu, dass dieselben Einschränkungen für Konstruktoren und Destruktoren statischer Objekte gelten, wenn die CRT gebunden ist.  2 3 4 5 6 7

  3. Microsoft Learn, Dynamic-Link Library Best Practices - Best Practices for Synchronization. Zur Struktur, die deadlockt, wenn Sie innerhalb von DllMain auf das Ende eines Threads warten (die DLL_THREAD_DETACH-Benachrichtigung des Thread-Endes braucht die Ladersperre); zum Protokoll zum Stoppen eines Threads beim Entladen (mit einem Ereignis signalisieren, einen konsistenten Zustand bestätigen, dann beenden); dazu, dass DLL_PROCESS_DETACH am Prozessende die anderen Threads bereits zwangsweise beendet hat und keine Garantie der Adressraumkonsistenz besteht, sodass der ideale Handler leer ist; sowie dazu, dass das Anlegen eines Threads in DllMain Benachrichtigungen mit unvollständiger Initialisierung in der Warteschlange lässt und Probleme verursacht.  2 3 4

  4. Microsoft Learn, Initialization of Mixed Assemblies. Dazu, MSIL nicht unter der Ladersperre auszuführen; DllMain und seinen Aufrufbaum nicht nach MSIL zu kompilieren und das über #pragma unmanaged zu behandeln; dazu, dass die Warnung C4747 ausgegeben wird, wenn DllMain versucht, MSIL direkt auszuführen, indirekte Ausführung über ein anderes Modul aber nicht erkennbar ist; sowie dazu, dass dynamische Initialisierer statischer Objekte dasselbe Problem verursachen können.  2

  5. Microsoft Learn, DisableThreadLibraryCalls function (libloaderapi.h). Zum Deaktivieren der Benachrichtigungen DLL_THREAD_ATTACH / DLL_THREAD_DETACH, um den Aufwand bei Thread-Anlage und -Zerstörung zu senken; dazu, es nicht aus einer mit der statischen CRT gebundenen DLL aufzurufen; sowie dazu, dass die Optimierung nicht ausgeführt wird, wenn statisches TLS (thread_local oder __declspec(thread)) in Kraft ist.  2

  6. Microsoft Learn, Dynamic-Link Library Best Practices - Deadlocks Caused by Lock Order Inversion. Zum Definieren einer Sperrhierarchie und immer im selben Reihenfolge erwerben; dazu, dass der Lader die Ladersperre erwirbt, bevor er DllMain aufruft, sodass die Ladersperre an der Spitze der Sperrhierarchie sitzen sollte; zum Beachten der Erwerbsreihenfolge zwischen APIs, die die Ladersperre indirekt nehmen, wie GetModuleFileName, und privaten Sperren; sowie zu einem konkreten Beispiel eines Deadlocks durch Umkehrung der Sperrreihenfolge.  2

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

Kann man in DllMain wirklich gar nichts tun?
„Nichts tun“ ist keine Übertreibung; es ist die offizielle Entwurfsposition, und Microsoft selbst sagt, dass die ideale DllMain ein nahezu leerer Stub ist. Sicher ist eine Teilmenge der Funktionen von Kernel32.dll — Kernel32 ist garantiert geladen, wenn DllMain läuft — in dem Bereich, der keine anderen DLLs lädt. Das Anlegen einer Critical Section oder eines Mutex und die Nutzung von TLS sind Beispiele für das, was Sie tun können. Umgekehrt sind LoadLibrary/FreeLibrary, das Synchronisieren mit anderen Threads und das Aufrufen von Funktionen in User32, Shell, COM und Ähnlichem verboten, weil sie Deadlocks und Zugriffsverletzungen verursachen. Initialisierung, bei der Sie unsicher sind, sollten Sie nicht in DllMain tun; schieben Sie sie bis zur ersten Nutzung auf.
Fallen Konstruktoren von C++-Globalen (statischen Objekten) auch unter die DllMain-Einschränkungen?
Ja. Wenn die DLL mit der CRT (der C++-Laufzeit) gebunden ist, laufen Konstruktoren und Destruktoren von globalen und statischen Objekten über den Einstiegspunkt, den die CRT bereitstellt, als de-facto-Teil von DllMain. Das heißt, LoadLibrary aus einem Konstruktor aufzurufen, einen anderen Thread zu starten und auf sein Ende zu warten, COM zu initialisieren und so weiter tragen alle dieselbe Gefahr wie das Tun derselben Dinge in DllMain. Für ein globales Objekt mit nichttrivialer Initialisierung halten Sie einen Zeiger und konstruieren Sie es beim ersten Zugriff, oder nutzen Sie eine funktionslokale static, sodass die Arbeit außerhalb von DllMain läuft.
Soll ich DisableThreadLibraryCalls aufrufen?
Bedingt ja. Wenn die DLL keine DLL_THREAD_ATTACH/DETACH-Benachrichtigungen braucht, stoppt der Aufruf von DisableThreadLibraryCalls in DLL_PROCESS_ATTACH die Benachrichtigungen pro Thread-Anlage und pro Thread-Ende und senkt den Aufwand in einem Prozess, der häufig Threads anlegt. Es gibt zwei Ausnahmen. Rufen Sie es nicht aus einer DLL auf, die mit der statischen CRT gebunden ist (die statische CRT braucht die Thread-Benachrichtigungen). Und wenn statisches TLS über thread_local oder __declspec(thread) in Kraft ist, schlägt der Aufruf selbst fehl und gibt FALSE zurück, also machen Sie sich zur Gewohnheit, den Rückgabewert zu prüfen. Nutzen Sie es auf einer typischen DLL, die die dynamisch gebundene CRT nutzt, nachdem Sie bestätigt haben, dass nichts von den Thread-Benachrichtigungen abhängt.
Warum hängt eine C++/CLI-(gemischt verwaltete) DLL beim Start?
Die typische Ursache ist der Versuch, MSIL (verwalteten Code) auszuführen, während die Ladersperre gehalten wird. In einer gemischten C++/CLI-Assembly können, wenn DllMain, von ihr aufgerufene Funktionen oder dynamische Initialisierer von Globalen nach MSIL kompiliert sind, unter der Ladersperre die CLR-Initialisierung oder das Laden einer anderen Assembly nötig werden, und das kann deadlocken. Der Compiler gibt die Warnung C4747 aus, wenn DllMain selbst versucht, MSIL direkt auszuführen, aber er kann indirekte Ausführung über ein anderes Modul nicht erkennen. Die Gegenmaßnahme ist, DllMain und seinen Aufrufbaum mit #pragma unmanaged nativ zu kompilieren — oder gar keine DllMain zu haben.
Darf ich Ressourcen in DLL_PROCESS_DETACH aufräumen?
Die Antwort ändert sich zwischen „Prozessende“ und „Entladen über FreeLibrary“. Bei DLL_PROCESS_DETACH am Prozessende sind die anderen Threads bereits beendet, und es gibt keine Garantie, dass der Adressraum noch konsistent ist, sodass Aufräumen wie das Freigeben von Speicher tatsächlich gefährlich ist; die offizielle Leitlinie ist, dass „der ideale Handler leer ist“. Schreiben Sie Daten, die erhalten bleiben müssen, im eigenen Beendigungspfad der App heraus, und tun Sie hier im Wesentlichen nichts und kehren Sie zurück. Bei einem Entladen über FreeLibrary läuft der Prozess weiter, also brauchen Sie vollständiges Aufräumen — Threads stoppen, Handles schließen und so weiter. Das Warten auf das Ende eines Threads innerhalb von DllMain deadlockt allerdings, also müssen Sie das offizielle Protokoll befolgen: signalisieren, bis zu einem konsistenten Zustand warten und die Arbeit außerhalb von DllMain beenden.

Autorenprofil

Profilseite des Artikelautors.

Go Komura

Geschäftsführer von KomuraSoft LLC

Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.

Zurück zum Blog