DllMain und die Ladersperre — Der wahre Grund, warum man sagt, in der DLL-Initialisierung nichts zu tun
· Aktualisiert am: · Go Komura · Windows, DLL, Windows-Entwicklung, C++, Fehleruntersuchung, Multithreading, Win32-API
Änderungsverlauf (Erstfassung, veröffentlicht am 22. Aug 2026)
- Erstveröffentlichung
Diesen Artikel zitieren(DOI (registriertes Archiv): 10.5281/zenodo.22176697)
Die folgenden DOIs verweisen auf bereits archivierte Versionen, die vom aktuellen Text abweichen können. Verwenden Sie die URL dieser Seite, um auf den aktuellen Text zu verweisen.
Go Komura (2026). DllMain und die Ladersperre — Der wahre Grund, warum man sagt, in der DLL-Initialisierung nichts zu tun. KomuraSoft LLC. https://comcomponent.com/de/blog/dllmain-loader-lock/
- DOI (registriertes Archiv)
- 10.5281/zenodo.22176697
- DOI (zuletzt registrierte Version)
- 10.5281/zenodo.22176698
„Wenn wir unsere eigene DLL laden, kehrt LoadLibrary nicht zurück.“ „Es hängt nur auf bestimmten PCs, oder nur beim Start des Dienstes.“ Bei solchen Störungen ist einer der ersten Orte, die Sie prüfen sollten, der Initialisierungscode der DLL: DllMain und alles, was von dort aufgerufen wird.
DllMain ist nicht wie eine gewöhnliche Initialisierungsfunktion einer App. Es ist eine Funktion, die das Betriebssystem aufruft, während es die Ladersperre hält, daher ist stark eingeschränkt, was sie tun darf. Microsofts Haltung, dass „die ideale DllMain ein leerer Stub ist“, kommt daher, dass diese Einschränkung nicht nur Ihre eigene DLL betrifft, sondern die anderen DLLs und Threads im Prozess.1
Dieser Artikel richtet sich an Entwickler, die unter Windows DLLs, Plug-ins und C++/CLI-Wrapper schreiben. Er arbeitet das Thema in dieser Reihenfolge durch: warum es stehen bleibt, dann wohin die Initialisierung gehört, dann wie man beendet, dann wie man einen Hänger untersucht.
1. Zuerst das Fazit — DllMain klein halten und den Ausführungszeitpunkt ändern
Statt nach einer sicheren Schreibweise innerhalb von DllMain zu suchen, ist der Grundansatz, die Arbeit zu reduzieren, die dort läuft. Was bleibt, entscheiden Sie in dieser Reihenfolge.1
| Entscheidung | Grundsatz | Wo Sie mehr lesen |
|---|---|---|
| Wann initialisieren | Was zur Kompilierzeit festliegt, statisch erledigen; den Rest bis zur ersten Nutzung nach abgeschlossenem Laden aufschieben | Kapitel 5 |
| Was Sie aus DllMain aufrufen | LoadLibrary / FreeLibrary, Synchronisation mit anderen Threads und die Nutzung von User, Shell, COM und Ähnlichem vermeiden. Auch indirekte Aufrufe prüfen |
Kapitel 3 |
| Was Sie beim Beenden tun | Den Fall, in dem nur die DLL entladen wird, vom Fall trennen, in dem der ganze Prozess endet | Kapitel 6 |
Leicht zu übersehen ist, dass dieselben Einschränkungen für die Konstruktoren und Destruktoren globaler und statischer C++-Objekte gelten. In C++/CLI beseitigen Sie außerdem jeden Pfad, der unter der Ladersperre MSIL ausführt. Ob der DllMain-Rumpf selbst leer ist, reicht zur Beurteilung nicht.23
Wenn Sie gerade einen Hänger untersuchen, beginnen Sie mit Kapitel 7; wenn Sie einen Entwurf prüfen, lesen Sie die Kapitel 2 bis 6 der Reihe nach, und Sie können jedes Verbot seiner Gegenmaßnahme zuordnen.
2. Der Mechanismus — DllMain wird innerhalb der Ladersperre aufgerufen
2.1 Es wird nicht nur beim Laden aufgerufen, sondern auch beim Start und Ende von Threads
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.2
| Benachrichtigung | Zeitpunkt |
|---|---|
| DLL_PROCESS_ATTACH | Wenn die DLL in den Prozess geladen wird |
| DLL_THREAD_ATTACH | Wenn im Prozess ein neuer Thread startet |
| DLL_THREAD_DETACH | Wenn ein Thread normal endet |
| DLL_PROCESS_DETACH | Wenn die DLL entladen wird oder der Prozess endet |
Eine Thread-Start-Benachrichtigung geht nicht nur an die DLL, die den Thread angelegt hat, sondern an jede im Prozess geladene DLL. DllMain ist nicht „Code, der einmal läuft, wenn meine DLL geladen wird“. In einem Prozess, der häufig Threads anlegt, läuft sie bei jeder Benachrichtigung.2
Wenn Sie die Benachrichtigungen nicht brauchen, ist eine Option, innerhalb von DLL_PROCESS_ATTACH DisableThreadLibraryCalls aufzurufen. In einer mit der statischen CRT gebundenen DLL ist das jedoch nicht nutzbar, und statisches TLS stellt eine eigene Bedingung, daher behandelt Abschnitt 5.3 diese Entscheidung getrennt.4
2.2 Der Aufruf bei gehaltener Sperre ist der Ausgangspunkt aller Einschränkungen
Damit Laden, Entladen und Benachrichtigungen von DLLs konsistent bleiben, serialisiert der OS-Lader sie mit einer Ladersperre pro Prozess. Der wichtige Punkt ist, dass er diese Sperre erwirbt, bevor er DllMain aufruft, und sie hält, während DllMain läuft.1
In dieser Zeit wartet jeder andere Thread desselben Prozesses, der eine DLL laden oder eine Thread-Start- oder -Ende-Benachrichtigung fortsetzen will, auf die Freigabe der Sperre. Das ist kein Ort, an dem Sie nur weil es Ihrer eigenen DLL gelegen kommt, ein langes Warten einfügen dürfen.
flowchart TB
accTitle: Warum Arbeit in DllMain die DLL-Benachrichtigungen im ganzen Prozess betrifft
accDescr: Der Lader erwirbt die prozessweite Ladersperre, bevor er DllMain aufruft, und DLL-Laden sowie Thread-Benachrichtigungen auf anderen Threads warten auf ihre Freigabe, sodass Arbeit in DllMain auch andere DLLs und Threads betrifft
loader["Lader erwirbt die gemeinsame Sperre"] --> dll["DllMain wird ausgeführt"]
dll --> ret["DllMain kehrt zurück"]
ret --> unlock["Ladersperre wird freigegeben"]
other["DLL-Laden oder Benachrichtigung auf einem anderen Thread"] --> wait["Wartet auf die Freigabe derselben Sperre"]
unlock --> resume["Wartende Arbeit kann fortfahren"]
wait --> resume
Abbildung 1: Während DllMain läuft, wird die gemeinsame Sperre gehalten, daher hält ein Warten dort den Fortschritt anderer DLLs und Threads auf.
Die folgenden Verbote werden verständlich, sobald Sie fragen: „Braucht diese Arbeit direkt oder indirekt die Ladersperre oder die Initialisierung einer anderen DLL?“
3. Warum es stehen bleibt — die Verbote über vier Pfade verstehen
3.1 Etwas aufrufen, das eine andere DLL lädt
Vermeiden Sie den Aufruf von LoadLibrary / FreeLibrary aus DllMain. LoadLibrary erzeugt zirkuläre Abhängigkeiten in der Ladereihenfolge und führt dazu, dass eine DLL genutzt wird, bevor ihr Initialisierungscode gelaufen ist. Auf der Beendigungsseite besteht ebenso die Gefahr, eine DLL zu nutzen, die bereits verarbeitet oder freigegeben wurde.2
Dass Sie selbst kein LoadLibrary geschrieben haben, macht Sie nicht sicher. Einige User-, Shell- und COM-Funktionen laden intern andere Systemkomponenten und können eine noch nicht initialisierte oder bereits freigegebene Komponente anfassen und eine Zugriffsverletzung verursachen.2
Die Basis dessen, was sicher aufgerufen werden kann, sind die Funktionen in Kernel32.dll, die keine anderen DLLs laden, weil Kernel32.dll zum Zeitpunkt von DllMain garantiert geladen ist. Sie können zum Beispiel Synchronisierungsobjekte wie Critical Sections und Mutexes anlegen und TLS nutzen. Die offizielle Dokumentation stellt jedoch ausdrücklich fest, dass keine erschöpfende Liste sicherer Funktionen existiert. Denken Sie nicht „es ist in Kernel32, also ist alles erlaubt“ oder „ich kann ein Synchronisierungsobjekt anlegen, also darf ich auf andere Threads warten“.2
3.2 Innerhalb von DllMain auf das Ende eines anderen Threads warten
Der klassische Deadlock entsteht beim Entladen der DLL: DllMain fordert einen Worker-Thread auf zu stoppen und wartet dann auf sein Ende.
Selbst nachdem der Worker seine eigene Arbeit beendet hat, muss er die DLL_THREAD_DETACH-Benachrichtigung beim Thread-Ende durchlaufen. Diese Benachrichtigung braucht die Ladersperre, die die wartende DllMain hält. Im Ergebnis wartet DllMain auf den Worker, und der Worker wartet darauf, dass DllMain zurückkehrt.5
sequenceDiagram
accTitle: Warum das Warten auf einen Thread in DllMain deadlockt
accDescr: DllMain hält die Ladersperre und wartet auf das Ende eines Worker-Threads, aber der endende Worker-Thread wartet für seine DLL_THREAD_DETACH-Benachrichtigung auf die Freigabe der Ladersperre, sodass beide aufeinander warten und deadlocken
participant L as Lader (hält die Sperre)
participant D as DllMain
participant W as Worker-Thread
L->>D: DLL_PROCESS_DETACH benachrichtigen
D->>W: Ende anfordern und auf Abschluss warten
W->>W: Arbeit beenden und zum Thread-Ende gehen
Note over W: Die Ende-Benachrichtigung braucht die Ladersperre
Note over D,W: DllMain wartet und hält die Sperre, W wartet auf die Sperre
Abbildung 2: Selbst nachdem der Worker seine Arbeit beendet hat, kommt er nicht durch die Thread-Ende-Benachrichtigung, daher löst sich das Ende-Warten innerhalb von DllMain nicht auf.
Das ist kein Fall von „es bleibt stehen, wenn Sie Pech haben“; das gegenseitige Warten hält strukturell. Das Aufräumen, das beim Entladen nötig ist, behandelt Kapitel 6 getrennt von der Arbeit am Prozessende.
3.3 Die Erwerbsreihenfolge der eigenen Sperre und der Ladersperre kehrt sich um
Es geht nicht nur um Thread-Start und -Ende; APIs wie GetModuleHandle brauchen intern ebenfalls die Ladersperre. Wenn die folgenden zwei Pfade überlappen, kehrt sich die Erwerbsreihenfolge der Sperren um.6
- Auf der DllMain-Seite versucht Code, der bereits die Ladersperre hält, die private Sperre G zu nehmen.
- Auf der Worker-Seite ruft Code, der bereits die private Sperre G hält, eine API auf und versucht, die Ladersperre zu nehmen.
flowchart TB
accTitle: Reihenfolgeumkehr zwischen Ladersperre und privater Sperre
accDescr: DllMain geht bei gehaltener Ladersperre an eine private Sperre, und ein Worker-Thread geht bei gehaltener privater Sperre an die Ladersperre, etwa für GetModuleHandle, sodass die Erwerbsreihenfolge umgekehrt ist und sie deadlocken
d["DllMain: hält die Ladersperre"] --> dg["Geht an die private Sperre G"]
w["Worker: hält die private Sperre G"] --> wl["Geht an die Ladersperre"]
dg -.-> dead["Deadlock durch umgekehrte Erwerbsreihenfolge"]
wl -.-> dead
wl -.-> api["Intern von GetModuleHandle und anderen verlangt"]
Abbildung 3: Nimmt die eine Seite Ladersperre und dann G, die andere G und dann die Ladersperre, wartet jede auf die Freigabe der anderen.
Die offizielle Leitlinie verlangt, die Ladersperre als die Spitze der Sperrhierarchie der App zu behandeln, also als die zuerst erworbene Sperre. In dem Moment, in dem Sie in DllMain sind, ist diese Sperre bereits gehalten. Prüfen Sie nicht nur den Namen der aufgerufenen Funktion, sondern auch welche Sperren Sie halten, wenn Sie sie aufrufen.6
3.4 Schon das bloße Anlegen eines Threads hinterlässt Startwarte- und Lebensdauerprobleme
Auch CreateThread innerhalb von DllMain wird nicht empfohlen. Der neue Thread kann seine Thread-Funktion erst ausführen, wenn die DLL_THREAD_ATTACH-Benachrichtigung verarbeitet ist. Weil die aktuelle DllMain die Ladersperre hält, deadlockt das Warten innerhalb dieser DllMain auf den Start oder Abschluss des Threads.5
Nicht zu warten löst auch nicht alles. Wird die DLL entladen, nachdem DllMain zurückgekehrt ist, der von Ihnen angelegte Thread aber noch nicht zu laufen begonnen hat, zeigt die Startadresse des Threads auf bereits freigegebenen Code, und ein Absturz bleibt möglich.5
flowchart TB
accTitle: Zwei Probleme, die beim Anlegen eines Threads in DllMain bleiben
accDescr: Ein in DllMain angelegter Thread wartet für seine Startbenachrichtigung auf die Ladersperre, sodass DllMain deadlockt, wenn sie auf Start oder Abschluss wartet; kehrt DllMain ohne Warten zurück, endet die Lebensdauer des Codes und es stürzt ab, wenn die DLL entladen wird, bevor der Thread zu laufen beginnt
create["Thread innerhalb von DllMain angelegt"] --> pending["Neuer Thread wartet auf die Startbenachrichtigung"]
pending --> q{"Innerhalb von DllMain auf Start oder Abschluss warten?"}
q -->|"Ja"| dead["Gegenseitiges Warten bei gehaltener Sperre"]
q -->|"Nein"| returns["Rückkehr aus DllMain"]
returns --> race["DLL entladen, bevor der Thread zu laufen beginnt"]
race --> crash["Code an der Startadresse ist weg"]
Abbildung 4: Innerhalb von DllMain nicht zu warten und die Lebensdauer der DLL zu schützen, die der angelegte Thread nutzt, sind zwei getrennte Anforderungen.
4. Zwei Stellen, die auch bei leerer DllMain gefährlich sind
4.1 Dynamische Initialisierung globaler und statischer Objekte
In einer mit der CRT (der C/C++-Laufzeit) gebundenen DLL laufen die Konstruktoren und Destruktoren globaler und statischer C++-Objekte über den Einstiegspunkt der CRT. Sie sind faktisch Teil von DllMain und unterliegen denselben Einschränkungen.2
Das Laden einer Konfigurationsdatei, den Start eines Loggers, COM-Initialisierung oder Thread-Start in einen Konstruktor zu legen versteckt nur den Aufrufort; der Ausführungszeitpunkt bleibt innerhalb der Ladersperre. Enthält diese komplexe Arbeit das Laden einer anderen DLL oder Thread-Synchronisation, ist sie genau so gefährlich, als stünde sie im DllMain-Rumpf.
flowchart TB
accTitle: Wie die Initialisierung globaler Objekte zur Mine wird
accDescr: Das Laden der DLL erwirbt die Ladersperre und die Konstruktoren globaler Objekte laufen über die CRT, sodass ein LoadLibrary, Thread-Synchronisation oder COM-Initialisierung darin die Ausführung dessen ist, was DllMain verbietet
load["DLL-Laden (Ladersperre erworben)"] --> crt["CRT-Einstiegspunkt"]
crt --> ctor["Konstruktor des globalen Objekts"]
ctor --> ng1["Arbeit gleich LoadLibrary"]
ctor --> ng2["Thread starten und auf Abschluss warten"]
ctor --> ng3["Nutzung von COM oder User32"]
ng1 -.-> risk["Alles davon sind DllMain-Verbote"]
ng2 -.-> risk
ng3 -.-> risk
Abbildung 5: Reviewen Sie nicht nur den DllMain-Rumpf, sondern auch Initialisierung und Beendigung statischer Objekte, die von der CRT aufgerufen werden.
Behandeln Sie kompilierzeitliche Konstanteninitialisierung, etwa alles, was constexpr sein kann, getrennt von komplexer Laufzeitinitialisierung. Schieben Sie dynamische Initialisierung, die Funktionsaufrufe enthält, auf und sorgen Sie dafür, dass auch der erste Zugriff außerhalb von DllMain geschieht.
4.2 MSIL, das in einem C++/CLI-Aufrufziel ausgeführt wird
Das Wrappen einer nativen DLL in C++/CLI behandelt Native DLLs aus C# aufrufen: C++/CLI-Wrapper vs. P/Invoke. In dieser Konfiguration achten Sie auf Pfade, die unter der Ladersperre MSIL (verwalteten Code) ausführen. Wenn die Ausführung des MSIL CLR-Initialisierung oder das Laden einer anderen Assembly erfordert, ist ein Deadlock möglich.3
Der Compiler gibt die Warnung C4747 für Code aus, in dem DllMain direkt versucht, MSIL auszuführen. Indirekte Ausführung über eine Funktion in einem anderen Modul kann er nicht erkennen. Das Fehlen der Warnung allein ist keine Grundlage, den Code für sicher zu erklären. Dynamische Initialisierer statischer Objekte gehören ebenfalls in den Prüfbereich.3
flowchart TB
accTitle: Ob MSIL-Ausführung unter der Ladersperre erkennbar ist
accDescr: Code, in dem DllMain MSIL direkt ausführt, kann der Compiler mit Warnung C4747 erkennen, indirekte Ausführung über eine Funktion in einem anderen Modul jedoch nicht, daher muss man sie durch Review des Aufrufbaums und durchgängig native Kompilierung verhindern
d2["Aufruf aus DllMain"] --> dir["Führt MSIL direkt aus"]
d2 --> ind["Führt über ein anderes Modul aus"]
dir --> c47["Erkennbar durch Warnung C4747"]
ind --> nc["Compiler kann es nicht erkennen"]
nc -.-> rv["Verhindern durch Review und #pragma unmanaged"]
Abbildung 6: Neben dem direkten Pfad, den C4747 erkennt, reviewen Sie auch Aufrufe, die über ein anderes Modul gehen.
Die Gegenmaßnahme ist, DllMain und jede von dort erreichbare Funktion mit #pragma unmanaged nativ zu kompilieren, oder gar keine DllMain zu haben. Auch bei Letzterem dürfen indirekte Pfade wie statische Initialisierer nicht übersehen werden.3
5. Entwurf der Initialisierung — statisch machen, aufschieben, nur das Minimum behalten
5.1 Bevor Sie etwas in DllMain lassen, prüfen Sie, ob sich der Ausführungszeitpunkt ändern lässt
Der offizielle Grundsatz ist, die Initialisierung, die Sie können, zur Kompilierzeit zu erledigen und den Rest so weit wie möglich aufzuschieben. Nur Arbeit, die früh als Ladefehler erkannt werden muss, bleibt als Ausnahme und im Minimum.1
Es kann zum Beispiel die Anforderung geben, das Laden der DLL selbst scheitern zu lassen, weil eine abhängige Konfigurationsdatei beschädigt ist. Auch dann engen Sie es auf „die nötige Arbeit versuchen und sofort scheitern“ ein, statt andere Initialisierung zuerst laufen zu lassen und danach zu scheitern. Das ist keine Ausnahme, die komplexe Initialisierung zur Ladezeit erlaubt.1
flowchart TB
accTitle: Entwurfsleitlinien für die DLL-Initialisierung
accDescr: Zuerst prüfen, ob die Initialisierung zur Kompilierzeit statisch sein kann; wenn nicht, standardmäßig bis zur ersten Nutzung aufschieben und in DllMain nur das Minimum lassen, das früh als Ladefehler erkannt werden muss
q1{"Zur Kompilierzeit entscheidbar?"} -->|"Ja"| s["Zur statischen Initialisierung machen"]
q1 -->|"Nein"| q2{"Muss der Fehler beim Laden erkannt werden?"}
q2 -->|"Nein"| lazy["Bis zur ersten Nutzung aufschieben (der Standard)"]
q2 -->|"Ja"| min["Nur das Minimum in DllMain tun"]
lazy -.-> once["Mit INIT_ONCE oder funktionslokaler static absichern"]
Abbildung 7: Statische und aufgeschobene Initialisierung zuerst prüfen und in DllMain nur das Minimum lassen, das früh erkannt werden muss.
5.2 Aufgeschobene Initialisierung muss bis zu „wer ruft sie zuerst auf“ entworfen werden
Für den gegenseitigen Ausschluss bei der ersten Nutzung können Sie einmalige Initialisierung mit INIT_ONCE oder eine C++-funktionslokale static (eine Magic Static) nutzen. Die Idee ist, komplexe globale Objekte auf einen beim ersten Zugriff erzeugten Zeiger oder auf eine funktionslokale static zu verschieben.
Aufschieben allein bringt Sie jedoch nicht aus der Ladersperre heraus. Die Einschränkung entfällt erst, wenn der erste Zugriff von „einer gewöhnlichen API kommt, die nach abgeschlossenem DLL-Laden aufgerufen wird“. Dann kann es als gewöhnliche Initialisierung entworfen werden, die nahezu die gesamte Windows-API nutzen darf.1
Umgekehrt: geschieht dieser erste Zugriff aus DllMain oder einem statischen Initialisierer, läuft die Initialisierung am Ende doch unter der Ladersperre. Über das Herausziehen einer Initialisierungsfunktion hinaus bestätigen Sie wer sie zuerst aufruft, und wann.
flowchart TB
accTitle: Wann aufgeschobene Initialisierung aus der Ladersperre herauskommt
accDescr: Kommt der erste Zugriff der aufgeschobenen Initialisierung von einer gewöhnlichen API nach abgeschlossenem Laden, kann außerhalb der Ladersperre initialisiert werden; kommt er aus DllMain oder einem statischen Initialisierer, läuft sie unter denselben Einschränkungen
first{"Woher kommt der erste Zugriff?"}
first -->|"Gewöhnliche API nach abgeschlossenem Laden"| outside["Außerhalb der Ladersperre initialisieren"]
first -->|"DllMain oder statischer Initialisierer"| inside["Unter denselben Einschränkungen initialisieren"]
outside --> once["Mit INIT_ONCE oder funktionslokaler static absichern"]
inside --> move["Auch den Zeitpunkt des ersten Zugriffs verschieben"]
Abbildung 8: INIT_ONCE und funktionslokale statics übernehmen den gegenseitigen Ausschluss der Initialisierung; sie garantieren nicht, dass sie außerhalb der Ladersperre aufgerufen wird.
5.3 DisableThreadLibraryCalls anhand von drei Bedingungen entscheiden
In einer DLL, die nicht von Thread-Benachrichtigungen abhängt, stoppt der Aufruf von DisableThreadLibraryCalls in DLL_PROCESS_ATTACH die Benachrichtigungen DLL_THREAD_ATTACH / DLL_THREAD_DETACH. In einem Prozess, der häufig Threads anlegt, senkt das den Benachrichtigungsaufwand.4
Vor der Anwendung prüfen Sie die statische CRT, statisches TLS und ob irgendetwas die Benachrichtigungen nutzt. Eine mit der statischen CRT gebundene DLL darf es nicht aufrufen, weil die CRT selbst die Thread-Benachrichtigungen braucht. Wenn statisches TLS über thread_local oder __declspec(thread) in Kraft ist, schlägt der Aufruf selbst fehl und gibt FALSE zurück.4
flowchart TB
accTitle: Ob DisableThreadLibraryCalls aufgerufen werden soll
accDescr: Eine mit der statischen CRT gebundene DLL darf es nicht aufrufen, und bei statischem TLS schlägt der Aufruf selbst fehl, daher wird er nicht genutzt; eine DLL, die keines von beiden ist und keine Thread-Benachrichtigungen nutzt, kann es in DLL_PROCESS_ATTACH aufrufen, den Rückgabewert prüfen und den Benachrichtigungsaufwand senken
q1{"Mit der statischen CRT gebunden?"} -->|"Ja"| no2["Darf nicht aufgerufen werden"]
q1 -->|"Nein"| q2{"Nutzt statisches TLS?"}
q2 -->|"Ja"| eff["Aufruf schlägt sowieso fehl (FALSE)"]
q2 -->|"Nein"| q3{"Braucht Thread-Benachrichtigungen?"}
q3 -->|"Nein"| yes["In ATTACH aufrufen (Rückgabewert prüfen)"]
q3 -->|"Ja"| keep["Nicht aufrufen; die Benachrichtigungen behandeln"]
Abbildung 9: Unterscheiden Sie die statische CRT, wo es nicht genutzt werden darf, von statischem TLS, wo es fehlschlägt, und prüfen Sie den Rückgabewert auch dann, wenn Benachrichtigungen unnötig sind.
Es ist eine Optimierung für eine typische DLL, die die dynamisch gebundene CRT nutzt und diese Bedingungen erfüllt. DisableThreadLibraryCalls existiert, um Benachrichtigungen zu reduzieren; es ist kein Mittel, komplexe Initialisierung in DllMain zu tun.
6. Beendigung — DLL-Entladen und Prozessende trennen
6.1 Derselbe DLL_PROCESS_DETACH, aber was danach bleibt, unterscheidet sich
DLL_PROCESS_DETACH wird sowohl geliefert, wenn nur die DLL entladen wird, als auch wenn der ganze Prozess endet. Die Benachrichtigung hat denselben Namen, aber die Voraussetzungen für das Aufräumen unterscheiden sich.5
Beim Entladen über FreeLibrary läuft der Prozess danach weiter. Deshalb müssen Threads gestoppt und offene Handles, reservierte Ressourcen, Zustand, der erhalten bleiben muss, und so weiter ordentlich aufgeräumt werden. Wie Abschnitt 3.2 zeigte, deadlockt jedoch das Warten innerhalb von DllMain auf das natürliche Ende eines Threads zu diesem Zweck.5
Noch besser vermeiden Sie einen Entwurf, in dem eine entladbare DLL überhaupt Threads besitzt; Thread-Besitz auf die EXE-Seite zu verlagern ist die sicherste Option. Bei einem bestehenden Entwurf, in dem die DLL Threads besitzt, betrachten Sie das folgende Stoppprotokoll zusammen mit seinen Einschränkungen, nie eines ohne das andere.
6.2 Das offiziell dokumentierte Stoppprotokoll, wenn die DLL einen Worker besitzt
Microsofts Best Practices dokumentieren ein Stoppverfahren beim Entladen, das nicht auf „das natürliche Ende des Threads“ wartet, sondern auf „ein Signal, dass er einen konsistenten Zustand erreicht hat“.5
- Die DllMain-Seite signalisiert dem Worker über ein Ereignis, zu stoppen.
- Der Worker fährt seine laufende Arbeit bis zu einem konsistenten Zustand herunter, signalisiert den Abschluss und geht in ein unendliches Warten.
- Die DllMain-Seite bestätigt den konsistenten Zustand und beendet den Thread mit
TerminateThread.
Es wirkt grob, ist aber unter der Einschränkung dokumentiert, dass das Warten auf ein natürliches Ende die Ende-Benachrichtigung mit der Ladersperre kollidieren lässt. Die Voraussetzung ist, dass auch die Arbeit, den konsistenten Zustand zu erreichen, denselben Einschränkungen wie DllMain gehorcht. Tritt diese Arbeit in das Laden einer anderen DLL oder in ein Warten auf die Ladersperre ein, ist ein Deadlock mit der auf das Signal wartenden Seite unvermeidlich.5
sequenceDiagram
accTitle: Beim Entladen auf Konsistenzabschluss statt auf natürliches Ende warten
accDescr: DllMain signalisiert dem Worker zu stoppen, der Worker erreicht unter denselben Einschränkungen wie DllMain einen konsistenten Zustand, signalisiert zurück und geht in unendliches Warten, und DllMain bestätigt den konsistenten Zustand, bevor sie den Thread beendet
participant D as DllMain (beim Entladen)
participant W as Worker-Thread
D->>W: Stopp mit einem Ereignis signalisieren
W->>W: Unter denselben Einschränkungen in einen konsistenten Zustand
W->>D: Erreichte Konsistenz signalisieren
W->>W: In unendliches Warten gehen
D->>W: Konsistenz bestätigen, dann TerminateThread
Note over D,W: Gewartet wird auf das Konsistenzsignal, nicht auf natürliches Ende
Abbildung 10: Auch bei diesem Protokoll darf die Arbeit, die den konsistenten Zustand herstellt, nicht auf die Ladersperre warten.
Das heißt nicht, dass ein beliebiger laufender Thread einfach abgeschnitten werden darf. Prüfen Sie zuerst, ob der Thread außerhalb der DLL besessen werden kann.
6.3 Am Prozessende ist das Ideal, ohne etwas zu tun zurückzukehren
Wenn DLL_PROCESS_DETACH am Prozessende geliefert wird, sind die anderen Threads bereits beendet oder zwangsweise beendet, und auf die Konsistenz des Adressraums ist kein Verlass. Der Zustand abhängiger DLLs und Laufzeiten ist ebenfalls nicht vertrauenswürdig, sodass Aufräumen wie das Freigeben von Speicher eher gefährlich als hilfreich wird. Die offizielle Leitlinie sagt ebenfalls, dass der ideale Handler in diesem Fall leer ist.5
Schreiben Sie Daten, die erhalten bleiben müssen, im eigenen Beendigungscode der App heraus. Nutzen Sie DLL_PROCESS_DETACH nicht als letzten Ort, an dem noch alles aufgeräumt werden kann.
flowchart TB
accTitle: Aufräumvoraussetzungen unterscheiden sich zwischen DLL-Entladen und Prozessende
accDescr: Wird nur die DLL entladen, läuft der Prozess weiter, also müssen Ressourcen aufgeräumt werden, aber das Warten auf natürliches Ende innerhalb von DllMain vermeiden; am Prozessende nicht auf die Konsistenz anderer Threads und Ressourcen vertrauen, im Wesentlichen ohne etwas zu tun zurückkehren und zu speichernden Zustand vorher im Beendigungscode der App schreiben
reason{"Grund für DLL_PROCESS_DETACH?"}
reason -->|"Nur DLL-Entladen"| alive["Prozess läuft danach weiter"]
alive --> cleanup["Verbleibende Ressourcen ordentlich aufräumen"]
cleanup -.-> nojoin["Innerhalb von DllMain nicht auf natürliches Ende warten"]
reason -->|"Prozessende"| processEnd["Zustand anderer Threads und Ressourcen ist unsicher"]
processEnd --> empty["Im Wesentlichen ohne etwas zu tun zurückkehren"]
empty -.-> save["Vorher im Beendigungscode der App speichern"]
Abbildung 11: Entscheiden Sie nicht nur „ist Aufräumen nötig“, sondern ob der Prozess weiterläuft und ob die für das Aufräumen genutzten Ressourcen vertrauenswürdig sind.
7. Untersuchung eines Hängers — die wartende und die haltende Seite der Ladersperre finden
7.1 Im Dump das Paar der aufeinander wartenden Threads bestätigen
Nehmen Sie einen Dump im Moment des Einfrierens und prüfen Sie den Stack jedes Threads. Der typische Fingerabdruck ist ein Paar: ein Thread, der innerhalb einer Laderfunktion in ntdll.dll auf eine Sperre wartet, deren Name mit Ldr beginnt, und ein Thread, der innerhalb von DllMain oder einem statischen Initialisierer (dynamic initializer) auf etwas anderes wartet.
Ein Thread, der mitten in einem LoadLibrary-Aufruf steht, ist ein weiterer typischer Beteiligter. Sobald die Beziehung zwischen der auf die Sperre wartenden Seite und der Seite, die sie hält und auf etwas anderes wartet, zusammenhängt, können Sie nahezu sicher auf einen Ladersperren-Deadlock schließen.
flowchart TB
accTitle: Gegenseitiges Warten auf die Ladersperre aus einem Hänger-Dump bestätigen
accDescr: In den Stacks der Threads nach einem Sperrwarten in Ldr-Funktionen und einem Warten innerhalb von DllMain oder einem statischen Initialisierer suchen, bestätigen, dass beide aufeinander warten, und wenn das typische Paar nicht erscheint, auch andere Arten von Hängern untersuchen
dump["Dump während des Hängers nehmen"] --> stacks["Stack jedes Threads prüfen"]
stacks --> ldr["Sperrwarten in Ldr-Funktionen"]
stacks --> init["Warten innerhalb von DllMain oder einem statischen Initialisierer"]
ldr --> pair{"Hängt die Beziehung des gegenseitigen Wartens zusammen?"}
init --> pair
pair -->|"Ja"| found["Ladersperren-Deadlock"]
pair -->|"Nicht sichtbar"| more["Auch andere Arten von Hängern untersuchen"]
Abbildung 12: Entscheiden Sie nicht anhand eines einzelnen Stacks; schauen Sie auf das Paar aus der auf die Ladersperre wartenden Seite und der Seite, die sie hält und wartet.
Bedingungen wie „gelegentlich beim Start“, „nur auf einer bestimmten Maschine“ oder „nur wenn als Dienst ausgeführt“ sind ebenfalls Hinweise. Das gegenseitige Warten entsteht, wenn ein DLL-Laden mit einem Thread-Start oder -Ende zusammenfällt, daher ändern Umgebungsunterschiede und Ausführungszeitpunkt, wie es sich zeigt.
7.2 Mit Application Verifier und Review der Aufrufziele vorbeugen
Tests mit aktiviertem Application Verifier erkennen typische DllMain-Fehler zur Laufzeit.1 In C++/CLI ignorieren Sie die Warnung C4747 nicht und prüfen Sie die indirekten Pfade, die die Warnung nicht erkennt, aus der Sicht von Abschnitt 4.2.
Im Review suchen Sie unter der von DllMain erreichbaren Arbeit nach Funktionen, die LoadLibrary indirekt aufrufen. COM-Initialisierung, einige CRT-Funktionen und Delay-Load-Importe gehören zur Prüfung. Besonders leicht zu übersehen ist, dass der erste Aufruf einer delay-geladenen Importfunktion intern zu LoadLibrary wird. Auch wenn der Funktionsname außerhalb von DllMain liegt, bleibt die Einschränkung, solange der Aufrufer DllMain ist.
8. Zusammenfassung — nicht auf Funktionsnamen schauen, sondern auf „wann und unter welcher Sperre es läuft“
Die DllMain-Einschränkungen lassen sich alle von einem Punkt her verstehen: sie wird aufgerufen, während die prozessweite Ladersperre gehalten wird. Nicht nur Ihre eigene DllMain, sondern auch die statische Initialisierung und Beendigung der CRT und die indirekten Aufrufe von C++/CLI fallen in denselben Bereich.
Beginnen Sie eine Prüfung damit, ob Initialisierung statisch gemacht und der Rest bis nach abgeschlossenem Laden aufgeschoben werden kann. Prüfen Sie bis zum ersten Zugriff der aufgeschobenen Arbeit, und wenn Benachrichtigungen unnötig sind, erwägen Sie DisableThreadLibraryCalls nach Prüfung der Bedingungen zu statischer CRT und statischem TLS.
Auf der Beendigungsseite trennen Sie ein reines DLL-Entladen von einem Prozessende. Besitzt die DLL einen Worker, folgen Sie dem Protokoll aus Signal, Konsistenzprüfung und Beendigung und verlagern Sie den Thread-Besitz nach Möglichkeit auf die EXE-Seite. Bringen Sie DllMain am Prozessende nahe an leer und legen Sie das Speichern von Daten in den eigenen Beendigungscode der App.
flowchart TB
accTitle: Die Reihenfolge zum Review von DllMain und dem, was sie aufruft
accDescr: Den Bereich einschließlich nicht nur DllMain, sondern auch statischer Initialisierung und indirekter Aufrufe prüfen, Initialisierung auf statisch oder nach dem Laden verschieben, Stoppen und Aufräumen je nach Beendigungsgrund entwerfen, dann mit Application Verifier und Dumps bestätigen
scope["DllMain, statische Initialisierung und Aufrufziele"] --> timing["Initialisierung auf statisch oder nach dem Laden verschieben"]
timing --> first["Auch den ersten Zugriff der aufgeschobenen Arbeit prüfen"]
first --> shutdown["Aufräumen je Beendigungsgrund entwerfen"]
shutdown --> verify["Mit Verifier, Warnungen und Dumps bestätigen"]
Abbildung 13: Verfolgen Sie nicht nur, was die Initialisierung tut, sondern auch wann sie läuft und ihre Lebensdauer beim Beenden, und Sie können die Verbote als eine Entwurfspolitik behandeln.
Die Frage an einem Grenzfall lautet: „Ist das Arbeit, die ich ausführen darf, während ich die Ladersperre halte?“ Fragwürdige Initialisierung nicht in DllMain zu stopfen, sondern den Ausführungszeitpunkt zu ändern, ist der Ausgangspunkt eines sicheren DLL-Entwurfs.
Weiterführende Artikel
- Wie die Windows-DLL-Namensauflösung funktioniert – Suchreihenfolge und SxS
- Native DLLs aus C# aufrufen: C++/CLI-Wrapper vs. P/Invoke
- Praktische Multithreading-Best-Practices: C++-Edition — Unfälle mit RAII und jthread strukturell beseitigen
- Scheinwecken — Warum Bedingungsvariablen „ohne Benachrichtigung“ aufwachen und wie Sie unter Windows richtig warten
- Absturz-Dumps mit WinDbg + SOS lesen — Ein praxistauglicher Leitfaden zur Analyse nach der Sammlung
- Grundlagenwissen zu COM STA/MTA – Threadmodelle und wie man Hänger vermeidet
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.
- Fehleruntersuchung und Ursachenanalyse
- Technische Beratung und Design-Review
- Windows-Anwendungsentwicklung
- Kontakt
Quellen
-
Microsoft Learn, Dynamic-Link Library Best Practices. Dazu, dass DllMain aufgerufen wird, während die Ladersperre gehalten wird, sodass die Funktionen, die aufgerufen werden 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 kompilierzeitlicher 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 ↩7
-
Microsoft Learn, DllMain entry point. Dazu, im Einstiegspunkt nur einfache Initialisierung und Beendigung auszuführen; warum LoadLibrary / FreeLibrary nicht aufgerufen werden dürfen (zirkuläre Ladereihenfolge und Nutzung einer DLL vor der Initialisierung oder nach der Beendigung); dazu, dass Kernel32.dll garantiert geladen ist, sodass sie in dem Bereich aufgerufen werden kann, der keine anderen DLLs lädt; dazu, dass keine erschöpfende Liste sicherer Funktionen existiert; 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
-
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 ↩3 ↩4
-
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 ↩3
-
Microsoft Learn, Dynamic-Link Library Best Practices - Best Practices for Synchronization. Zur Struktur, die deadlockt, wenn DllMain auf das Ende eines Threads wartet (die DLL_THREAD_DETACH-Benachrichtigung des Thread-Endes braucht die Ladersperre); zum Protokoll zum Stoppen von 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, keine Garantie der Adressraumkonsistenz besteht und 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 ↩5 ↩6 ↩7 ↩8
-
Microsoft Learn, Dynamic-Link Library Best Practices - Deadlocks Caused by Lock Order Inversion. Zum Definieren einer Sperrhierarchie und immer in derselben Reihenfolge erwerben; dazu, dass der Lader die Ladersperre erwirbt, bevor er DllMain aufruft, sodass die Ladersperre an der Spitze der Sperrhierarchie sitzen sollte; zum Einhalten 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
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Win32-Thread-Pool-API — Nebenläufigkeit ohne eigene Threads, mit CreateThreadpoolWork
Häufen sich in nativem Code die CreateThread-Aufrufe? Dieser Artikel erklärt anhand von Primärquellen die in Vista neu gestaltete Win32-T...
Was „Keine Rückmeldung“ wirklich ist — Wie Windows entscheidet, dass eine App hängt, und Entwürfe, die nicht hängen
„Keine Rückmeldung“ unter Windows ist ein Mechanismus, bei dem das Betriebssystem urteilt, dass ein Fenster 5 Sekunden lang keine Nachric...
Time Travel Debugging — Langlaufende Fehler, die sich nicht reproduzieren, aufzeichnen und zurückspulen
Ein Fehler, der nur einmal im Monat auftritt, hinterlässt im Absturz-Dump nur das Ergebnis. Mit Time Travel Debugging (TTD) in WinDbg zei...
Was nach dem Tod des Elternprozesses übrig bleibt — Kindprozesse in einem Job Object halten
Warum SDK-Helfer eine beendete UI überleben und Kamera oder COM-Port behalten. Kindprozesslebensdauer mit Job Object, KillOnJobClose und ...
Named Pipes in der Praxis — Windows-Standard-IPC von der Auslegung bis zur Sicherheit
Ein Praxisleitfaden zu Named Pipes, der Standard-Prozesskommunikation unter Windows. Der Artikel ordnet anhand von Primärquellen die Wahl...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
Fehleranalyse und Langzeitprobleme
Sporadische Fehler, Kommunikationsdiagnose, Langzeitabstürze und Tests von Fehlerpfaden.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Windows-App-Entwicklung
Geschäftsanwendungen, Geräteintegration und Kommunikationstools von den Anforderungen bis zur Umsetzung.
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, sondern die offizielle Entwurfspolitik, und Microsoft selbst sagt, dass die ideale DllMain ein nahezu leerer Stub ist. Sicher sind Funktionen von Kernel32.dll, das zum Zeitpunkt von DllMain garantiert geladen ist, soweit sie keine anderen DLLs laden. Sie können zum Beispiel Critical Sections und Mutexes anlegen und TLS nutzen. Umgekehrt sind LoadLibrary/FreeLibrary, Synchronisation mit anderen Threads und Aufrufe von User32, Shell, COM und Ähnlichem verboten, weil sie Deadlocks und Zugriffsverletzungen verursachen. Bei Initialisierung, der Sie unsicher sind, ist die richtige Antwort, sie nicht in DllMain zu tun, sondern bis zur ersten Nutzung aufzuschieben.
- 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 globaler und statischer Objekte über den Einstiegspunkt, den die CRT bereitstellt, faktisch als Teil von DllMain. 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 dasselbe in DllMain. Bei globalen Objekten mit komplexer Initialisierung verschieben Sie den Ausführungszeitpunkt aus DllMain heraus: behalten Sie einen Zeiger und erzeugen Sie das Objekt beim ersten Zugriff, oder nutzen Sie eine funktionslokale static.
- Soll ich DisableThreadLibraryCalls aufrufen?
- Unter Bedingungen ja. Wenn die DLL keine DLL_THREAD_ATTACH/DETACH-Benachrichtigungen braucht, stoppt der Aufruf von DisableThreadLibraryCalls in DLL_PROCESS_ATTACH die Benachrichtigung bei jeder Thread-Anlage und jedem Thread-Ende und senkt den Aufwand in einem Prozess, der häufig Threads anlegt. Es gibt jedoch zwei Ausnahmen. Rufen Sie es nicht in 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 in einer typischen DLL mit dynamisch gebundener CRT, 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, die von ihr aufgerufenen Funktionen oder die dynamischen 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 versucht, MSIL direkt auszuführen, kann indirekte Ausführung über ein anderes Modul aber 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 unterscheidet sich zwischen „Prozessende“ und „Entladen über FreeLibrary“. Bei DLL_PROCESS_DETACH am Prozessende sind die anderen Threads bereits zwangsweise 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 lautet, 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 offiziell beschriebene Protokoll befolgen: signalisieren, bis zu einem konsistenten Zustand warten und die letzte Arbeit außerhalb von DllMain erledigen.
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.