DllMain et le verrou du chargeur — la vraie raison pour laquelle on vous dit de « ne rien faire dans l'initialisation d'une DLL »

· Mis à jour le: · · Windows, DLL, Développement Windows, C++, Investigation de bugs, Multithreading, Win32 API

Historique des révisions (première version, publiée le 22 Aug 2026)
Première publication
Citer cet article(DOI (archive enregistrée): 10.5281/zenodo.22176701)

Les DOI ci-dessous renvoient à des versions déjà archivées et peuvent différer du texte actuel. Pour citer le texte actuel, utilisez l’URL de cette page.

Go Komura (2026). DllMain et le verrou du chargeur — la vraie raison pour laquelle on vous dit de « ne rien faire dans l'initialisation d'une DLL ». KomuraSoft LLC. https://comcomponent.com/fr/blog/dllmain-loader-lock/

DOI (archive enregistrée)
10.5281/zenodo.22176701
DOI (dernière version enregistrée)
10.5281/zenodo.22176702

« Lorsque nous chargeons notre propre DLL, LoadLibrary ne revient jamais. » « Cela ne se bloque que sur certains PC, ou seulement au démarrage du service. » Face à des pannes de ce type, l’un des premiers endroits à vérifier est le code d’initialisation de la DLL : DllMain et tout ce qu’il appelle.

DllMain n’est pas comme une fonction d’initialisation ordinaire d’une application. C’est une fonction que l’OS appelle en détenant le verrou du chargeur, donc ce qu’elle peut faire est fortement restreint. La position de Microsoft selon laquelle « le DllMain idéal est un stub vide » vient du fait que cette restriction n’affecte pas seulement votre propre DLL, mais les autres DLL et threads du processus.1

Cet article s’adresse aux développeurs qui écrivent des DLL, des plug-ins et des wrappers C++/CLI sous Windows. Il traite le sujet dans cet ordre : pourquoi ça s’arrête, puis où déplacer l’initialisation, puis comment s’arrêter, puis comment investiguer un blocage.

1. La conclusion d’abord — garder DllMain petit et changer le moment d’exécution

Plutôt que de chercher une façon sûre d’écrire quelque chose à l’intérieur de DllMain, l’approche de base est de réduire le travail qui s’y exécute. Décidez ce qui y reste dans l’ordre suivant.1

Décision Politique de base Où en lire plus
Quand initialiser Faire statiquement ce qui peut se décider à la compilation ; différer le reste à la première utilisation après la fin du chargement Chapitre 5
Quoi appeler depuis DllMain Éviter LoadLibrary / FreeLibrary, la synchronisation avec d’autres threads, et l’utilisation de User, Shell, COM et analogues. Vérifier aussi les appels indirects Chapitre 3
Quoi faire à l’arrêt Séparer le cas où seule la DLL est déchargée du cas où le processus entier se termine Chapitre 6

Ce qui est facile à manquer, c’est que les mêmes restrictions s’appliquent aux constructeurs et destructeurs des objets C++ globaux et statiques. En C++/CLI, vous éliminez aussi tout chemin qui exécute du MSIL sous le verrou du chargeur. Que le corps de DllMain lui-même soit vide ne suffit pas pour juger.23

Si vous investiguez un blocage en ce moment, commencez au chapitre 7 ; si vous revoyez une conception, lisez les chapitres 2 à 6 dans l’ordre, et vous pourrez associer chaque interdiction à son remède.

2. Le mécanisme — DllMain est appelé à l’intérieur du verrou du chargeur

2.1 Il est appelé non seulement au chargement, mais aussi au démarrage et à la sortie des threads

DllMain est le point d’entrée que le chargeur de l’OS appelle lorsqu’une DLL entre dans un processus ou un thread, ou en sort. Il y a quatre notifications.2

Notification Moment
DLL_PROCESS_ATTACH Lorsque la DLL est chargée dans le processus
DLL_THREAD_ATTACH Lorsqu’un nouveau thread démarre dans le processus
DLL_THREAD_DETACH Lorsqu’un thread se termine normalement
DLL_PROCESS_DETACH Lorsque la DLL est déchargée, ou lorsque le processus se termine

Une notification de démarrage de thread ne va pas seulement à la DLL qui a créé le thread, mais à chaque DLL chargée dans le processus. DllMain n’est pas « du code qui s’exécute une fois lorsque ma DLL est chargée ». Dans un processus qui crée des threads fréquemment, il s’exécute à chaque notification.2

Si vous n’avez pas besoin des notifications, une option est d’appeler DisableThreadLibraryCalls à l’intérieur de DLL_PROCESS_ATTACH. Il ne peut toutefois pas être utilisé dans une DLL liée avec la CRT statique, et le TLS statique impose une condition propre, donc la section 5.3 traite cette décision à part.4

2.2 Être appelé en détenant le verrou est le point de départ de toutes les restrictions

Pour garder cohérents les chargements, déchargements et notifications de DLL, le chargeur de l’OS les sérialise avec un seul verrou du chargeur par processus. Le point important est qu’il acquiert ce verrou avant d’appeler DllMain et qu’il continue de le détenir pendant que DllMain s’exécute.1

Pendant ce temps, tout autre thread du même processus qui essaie de charger une DLL ou de poursuivre une notification de démarrage ou de sortie de thread attend que le verrou soit libéré. Ce n’est pas un endroit où vous pouvez insérer une longue attente simplement parce que cela arrange votre propre DLL.

Pourquoi le travail dans DllMain affecte les notifications de DLL dans tout le processusLe chargeur acquiert le verrou du chargeur à l'échelle du processus avant d'appeler DllMain, et les chargements de DLL et les notifications de thread sur d'autres threads attendent sa libération, donc le travail dans DllMain affecte aussi les autres DLL et threadsLe chargeur acquiert le verrou partagéDllMain s'exécuteDllMain revientLe verrou du chargeur est libéréChargement de DLL ou notification sur un autre threadAttend que le même verrou soit libéréLe travail en attente peut avancer

Figure 1 : le verrou partagé est détenu pendant que DllMain s’exécute, donc une attente à cet endroit arrête la progression des autres DLL et threads.

Les interdictions qui suivent deviennent compréhensibles une fois que vous demandez : « ce travail a-t-il besoin, directement ou indirectement, du verrou du chargeur ou de l’initialisation d’une autre DLL ? »

3. Pourquoi ça s’arrête — comprendre les interdictions par quatre chemins

3.1 Appeler un traitement qui charge une autre DLL

Évitez d’appeler LoadLibrary / FreeLibrary depuis DllMain. LoadLibrary crée des dépendances circulaires dans l’ordre de chargement et conduit à utiliser une DLL avant que son code d’initialisation n’ait tourné. Du côté de l’arrêt, il y a de même le risque d’utiliser une DLL déjà traitée ou libérée.2

Ne pas avoir écrit LoadLibrary vous-même ne vous rend pas sûr. Certaines fonctions User, Shell et COM chargent en interne d’autres composants système, et elles peuvent toucher un composant pas encore initialisé ou déjà libéré et provoquer une violation d’accès.2

La base de ce qui peut être appelé en sécurité, ce sont les fonctions de Kernel32.dll qui ne chargent pas d’autres DLL, car Kernel32.dll est garanti chargé au moment où DllMain s’exécute. Vous pouvez par exemple créer des objets de synchronisation tels que des sections critiques et des mutex, et utiliser le TLS. La documentation officielle indique toutefois explicitement qu’il n’existe aucune liste exhaustive de fonctions sûres. Ne raisonnez pas « c’est dans Kernel32, donc tout est permis » ni « je peux créer un objet de synchronisation, donc je peux attendre d’autres threads ».2

3.2 Attendre à l’intérieur de DllMain qu’un autre thread se termine

L’interblocage classique se produit au déchargement de la DLL : DllMain demande à un thread worker de s’arrêter, puis attend qu’il se termine.

Même après que le worker a fini son propre travail, il doit passer par la notification DLL_THREAD_DETACH à la sortie du thread. Cette notification a besoin du verrou du chargeur, que le DllMain en attente détient. Le résultat est que DllMain attend le worker, et le worker attend que DllMain revienne.5

Pourquoi attendre un thread dans DllMain interbloqueDllMain, détenant le verrou du chargeur, attend qu'un thread worker se termine, mais le thread worker qui se termine attend que le verrou du chargeur soit libéré pour sa notification DLL_THREAD_DETACH, donc les deux s'attendent et interbloquentThread workerDllMainChargeur (détient le verrou)Thread workerDllMainChargeur (détient le verrou)La notification de sortie a besoin du verrou du chargeurDllMain attend en détenant le verrou, W attend le verrouNotifier DLL_PROCESS_DETACHDemander la sortie et attendre l'achèvementFinir le travail et se diriger vers la sortie du thread

Figure 2 : même après que le worker a fini son travail, il ne peut pas passer la notification de sortie de thread, donc l’attente de sortie à l’intérieur de DllMain ne se résout jamais.

Ce n’est pas un cas de « ça s’arrête si vous n’avez pas de chance » ; l’attente mutuelle tient structurellement. Le nettoyage nécessaire au déchargement est traité au chapitre 6, séparément du travail à la sortie du processus.

3.3 L’ordre d’acquisition de votre verrou et du verrou du chargeur s’inverse

Ce n’est pas seulement le démarrage et la sortie de thread ; des API telles que GetModuleHandle ont aussi besoin du verrou du chargeur en interne. Lorsque les deux chemins suivants se recouvrent, l’ordre d’acquisition des verrous s’inverse.6

  • Du côté DllMain, du code qui détient déjà le verrou du chargeur essaie de prendre le verrou privé G.
  • Du côté worker, du code qui détient déjà le verrou privé G appelle une API et essaie de prendre le verrou du chargeur.
Inversion d'ordre entre le verrou du chargeur et un verrou privéDllMain va chercher un verrou privé en détenant le verrou du chargeur, et un thread worker va chercher le verrou du chargeur, pour GetModuleHandle et analogues, en détenant le verrou privé, donc l'ordre d'acquisition s'inverse et ils interbloquentDllMain : détient le verrou du chargeurVa chercher le verrou privé GWorker : détient le verrou privé GVa chercher le verrou du chargeurInterblocage par ordre d'acquisition inverséExigé en interne par GetModuleHandle et d'autres

Figure 3 : lorsque l’un prend le verrou du chargeur puis G, et l’autre G puis le verrou du chargeur, chacun attend que l’autre libère.

Le guidage officiel demande de traiter le verrou du chargeur comme le sommet de la hiérarchie de verrous de l’application, c’est-à-dire le verrou acquis en premier. Au moment où vous êtes dans DllMain, ce verrou est déjà détenu. Vérifiez non seulement le nom de la fonction que vous appelez, mais aussi quels verrous vous détenez lorsque vous l’appelez.6

3.4 Même seulement créer un thread laisse des problèmes d’attente de démarrage et de durée de vie

CreateThread à l’intérieur de DllMain n’est pas recommandé non plus. Le nouveau thread ne peut pas commencer à exécuter sa fonction de thread tant que la notification DLL_THREAD_ATTACH n’a pas été traitée. Parce que le DllMain actuel détient le verrou du chargeur, attendre à l’intérieur de ce DllMain que le thread démarre ou se termine interbloque.5

Ne pas attendre ne résout pas tout non plus. Si la DLL est déchargée après le retour de DllMain mais avant que le thread que vous avez créé n’ait commencé à s’exécuter, l’adresse de démarrage du thread pointe vers du code déjà libéré, et un plantage reste possible.5

Deux problèmes qui restent lorsqu'un thread est créé dans DllMainUn thread créé dans DllMain attend le verrou du chargeur pour sa notification de démarrage, donc si DllMain attend son démarrage ou sa fin ils interbloquent, et même si DllMain revient sans attendre, la durée de vie du code s'arrête et il plante si la DLL est déchargée avant que le thread ne commence à s'exécuterOuiNonThread créé à l'intérieur de DllMainLe nouveau thread attend la notification de démarrageAttendre le démarrage ou l'achèvement à l'intérieur de DllMain ?Attente mutuelle en détenant le verrouRetour depuis DllMainDLL déchargée avant que le thread ne commence à s'exécuterLe code à l'adresse de démarrage a disparu

Figure 4 : ne pas attendre à l’intérieur de DllMain et protéger la durée de vie de la DLL qu’utilise le thread créé sont deux exigences distinctes.

4. Deux endroits dangereux même lorsque DllMain est vide

4.1 Initialisation dynamique des objets globaux et statiques

Dans une DLL liée avec la CRT (le runtime C/C++), les constructeurs et destructeurs des objets C++ globaux et statiques s’exécutent via le point d’entrée de la CRT. Ils font effectivement partie de DllMain et sont soumis aux mêmes restrictions.2

Mettre le chargement d’un fichier de configuration, le démarrage d’un journal, l’initialisation COM ou le démarrage d’un thread dans un constructeur ne fait que cacher l’endroit de l’appel ; le moment où cela s’exécute reste à l’intérieur du verrou du chargeur. Si ce travail complexe inclut le chargement d’une autre DLL ou une synchronisation de threads, c’est exactement aussi dangereux que de l’écrire dans le corps de DllMain.

Comment l'initialisation des objets globaux devient une mineLe chargement de la DLL acquiert le verrou du chargeur et les constructeurs des objets globaux s'exécutent via la CRT, donc un LoadLibrary, une synchronisation de threads ou une initialisation COM à l'intérieur est une exécution de ce que DllMain interditChargement de la DLL (verrou du chargeur acquis)Point d'entrée de la CRTConstructeur de l'objet globalTravail équivalent à LoadLibraryDémarrer un thread et attendre qu'il se termineUtilisation de COM ou de User32Tout cela est une interdiction de DllMain

Figure 5 : revoyez non seulement le corps de DllMain, mais aussi l’initialisation et la terminaison des objets statiques appelés depuis la CRT.

Traitez l’initialisation constante à la compilation, par exemple tout ce qui peut être constexpr, séparément d’une initialisation d’exécution complexe. Différez l’initialisation dynamique qui implique des appels de fonction, et faites en sorte que le premier accès se produise aussi hors de DllMain.

4.2 MSIL exécuté dans une cible d’appel C++/CLI

L’encapsulation d’une DLL native en C++/CLI est traitée dans Appeler une DLL native depuis C# : wrapper C++/CLI vs P/Invoke. Dans cette configuration, surveillez les chemins qui exécutent du MSIL (code géré) sous le verrou du chargeur. Si l’exécution du MSIL exige l’initialisation du CLR ou le chargement d’un autre assemblage, un interblocage est possible.3

Le compilateur émet l’avertissement C4747 pour le code dans lequel DllMain essaie directement d’exécuter du MSIL. Mais il ne peut pas détecter une exécution indirecte à travers une fonction d’un autre module. L’absence de l’avertissement à elle seule n’est pas une base pour dire que le code est sûr. Les initialiseurs dynamiques des objets statiques sont aussi dans le périmètre.3

Si l'exécution de MSIL sous le verrou du chargeur peut être détectéeLe code dans lequel DllMain exécute du MSIL directement peut être détecté par le compilateur avec l'avertissement C4747, mais une exécution indirecte à travers une fonction d'un autre module ne peut pas l'être, donc il faut la prévenir en revoyant l'arbre d'appels et en compilant nativement de bout en boutAppel depuis DllMainExécute du MSIL directementExécute via un autre moduleDétecté par l'avertissement C4747Le compilateur ne peut pas le détecterPrévenir par revue et #pragma unmanaged

Figure 6 : en plus du chemin direct que C4747 détecte, revoyez aussi les appels qui passent par un autre module.

Le remède est de compiler DllMain et toute fonction atteignable depuis celui-ci en natif avec #pragma unmanaged, ou de ne pas avoir de DllMain du tout. Même dans ce dernier cas, ne manquez pas les chemins indirects tels que les initialiseurs statiques.3

5. Concevoir l’initialisation — la rendre statique, la différer, ne garder que le minimum

5.1 Avant de laisser quelque chose dans DllMain, demandez-vous si le moment d’exécution peut changer

La ligne de base officielle est de terminer à la compilation l’initialisation que vous pouvez et de différer le reste autant que possible. Seul le travail qui doit être détecté tôt comme un échec de chargement reste, à titre d’exception et au minimum.1

Il peut par exemple y avoir l’exigence de faire échouer le chargement de la DLL elle-même parce qu’un fichier de configuration dont elle dépend est corrompu. Même alors, réduisez-le à « tenter le travail requis et échouer immédiatement » plutôt que de faire tourner d’abord d’autres initialisations et d’échouer ensuite. Ce n’est pas une exception qui autorise une initialisation complexe au chargement.1

Lignes directrices de conception pour l'initialisation d'une DLLD'abord examiner si l'initialisation peut être statique à la compilation ; sinon, différer par défaut à la première utilisation, et ne laisser dans DllMain que le minimum qui doit être détecté tôt comme un échec de chargementOuiNonNonOuiDécidable à la compilation ?En faire une initialisation statiqueL'échec doit-il être détecté au chargement ?Différer à la première utilisation (le défaut)Ne faire que le minimum dans DllMainProtéger avec INIT_ONCE ou un static local à une fonction

Figure 7 : examinez d’abord l’initialisation statique et différée, et ne laissez dans DllMain que le minimum qui a besoin d’une détection précoce.

5.2 L’initialisation différée doit être conçue jusqu’à « qui l’appelle en premier »

Pour l’exclusion mutuelle à la première utilisation, vous pouvez utiliser une initialisation unique avec INIT_ONCE ou un static local à une fonction C++ (un static magique). L’idée est de déplacer les objets globaux complexes vers un pointeur créé au premier accès ou vers un static local à une fonction.

Différer seulement, toutefois, ne vous sort pas du verrou du chargeur. La restriction n’est levée que lorsque le premier accès vient d’« une API ordinaire appelée après que le chargement de la DLL est terminé ». Dans ce cas, on peut la concevoir comme une initialisation ordinaire qui peut utiliser presque toute l’API Windows.1

Inversement, si ce premier accès se produit depuis DllMain ou un initialiseur statique, l’initialisation finit tout de même par s’exécuter sous le verrou du chargeur. Au-delà d’extraire une fonction d’initialisation, confirmez qui l’appelle en premier, et quand.

Quand l'initialisation différée sort du verrou du chargeurSi le premier accès de l'initialisation différée vient d'une API ordinaire après la fin du chargement, on peut initialiser hors du verrou du chargeur, mais s'il vient de DllMain ou d'un initialiseur statique, elle s'exécute sous les mêmes restrictionsAPI ordinaire après la fin du chargementDllMain ou un initialiseur statiqueD'où vient le premier accès ?Initialiser hors du verrou du chargeurInitialiser sous les mêmes restrictionsProtéger avec INIT_ONCE ou un static local à une fonctionDéplacer aussi le moment du premier accès

Figure 8 : INIT_ONCE et les statics locaux à une fonction s’occupent de l’exclusion mutuelle de l’initialisation ; ils ne garantissent pas qu’elle soit appelée hors du verrou du chargeur.

5.3 Décider de DisableThreadLibraryCalls selon trois conditions

Dans une DLL qui ne dépend pas des notifications de thread, appeler DisableThreadLibraryCalls dans DLL_PROCESS_ATTACH arrête les notifications DLL_THREAD_ATTACH / DLL_THREAD_DETACH. Dans un processus qui crée des threads fréquemment, cela réduit le surcoût des notifications.4

Avant de l’appliquer, vérifiez la CRT statique, le TLS statique, et si quelque chose utilise les notifications. Une DLL liée avec la CRT statique ne doit pas l’appeler, parce que la CRT elle-même a besoin des notifications de thread. Lorsque le TLS statique via thread_local ou __declspec(thread) est en vigueur, l’appel lui-même échoue et renvoie FALSE.4

Décider d'appeler DisableThreadLibraryCallsUne DLL liée avec la CRT statique ne doit pas l'appeler, et avec le TLS statique en vigueur l'appel lui-même échoue donc on ne l'appelle pas, mais une DLL qui n'est ni l'un ni l'autre et n'utilise pas les notifications de thread peut l'appeler dans DLL_PROCESS_ATTACH, en vérifiant la valeur de retour, pour réduire le coût des notificationsOuiNonOuiNonNonOuiLiée avec la CRT statique ?Ne doit pas l'appelerUtilise le TLS statique ?L'appel échoue de toute façon (FALSE)A besoin des notifications de thread ?L'appeler dans ATTACH (vérifier la valeur de retour)Ne pas l'appeler ; traiter les notifications

Figure 9 : distinguez la CRT statique, où il ne faut pas l’utiliser, du TLS statique, où il échoue, et vérifiez la valeur de retour même lorsque les notifications sont inutiles.

C’est une optimisation à considérer pour une DLL typique qui utilise la CRT liée dynamiquement et satisfait ces conditions. DisableThreadLibraryCalls existe pour réduire les notifications ; ce n’est pas un moyen de faire une initialisation complexe dans DllMain.

6. Arrêt — séparer le déchargement de la DLL de la sortie du processus

6.1 Le même DLL_PROCESS_DETACH, mais ce qui reste ensuite diffère

DLL_PROCESS_DETACH est délivré à la fois lorsque seule la DLL est déchargée et lorsque le processus entier se termine. La notification a le même nom, mais les prémisses du nettoyage diffèrent.5

Lors d’un déchargement via FreeLibrary, le processus continue ensuite. Donc les threads doivent être arrêtés, et les handles ouverts, les ressources allouées, l’état qui doit être persisté, etc., doivent être nettoyés correctement. Comme l’a montré la section 3.2, attendre à l’intérieur de DllMain la sortie naturelle d’un thread à cette fin interbloque toutefois.5

Mieux encore, évitez une conception dans laquelle une DLL qui peut être déchargée possède des threads ; déplacer la propriété des threads du côté de l’EXE est l’option la plus sûre. Pour une conception existante dans laquelle la DLL possède des threads, considérez le protocole d’arrêt suivant avec ses contraintes, jamais l’un sans l’autre.

6.2 Le protocole d’arrêt documenté officiellement lorsque la DLL possède un worker

Les meilleures pratiques de Microsoft documentent une procédure d’arrêt au déchargement qui attend non pas « la sortie naturelle du thread » mais « un signal qu’il a atteint un état cohérent ».5

  1. Le côté DllMain signale au worker de s’arrêter, au moyen d’un événement.
  2. Le worker ramène son travail en cours jusqu’à un état cohérent, signale l’achèvement et entre dans une attente infinie.
  3. Le côté DllMain confirme l’état cohérent et termine le thread avec TerminateThread.

Cela a l’air brutal, mais c’est documenté sous la contrainte que attendre une sortie naturelle fait collisionner la notification de sortie avec le verrou du chargeur. La prémisse est que le travail d’atteindre l’état cohérent obéit aussi aux mêmes restrictions que DllMain. Si ce travail entre dans le chargement d’une autre DLL ou dans une attente du verrou du chargeur, un interblocage avec le côté qui attend le signal est inévitable.5

Au déchargement, attendre l'achèvement de cohérence plutôt que la sortie naturelleDllMain signale au worker de s'arrêter, le worker atteint un état cohérent en obéissant aux mêmes restrictions que DllMain, signale en retour et entre dans une attente infinie, et DllMain confirme l'état cohérent avant de terminer le threadThread workerDllMain (au déchargement)Thread workerDllMain (au déchargement)Ce qu'on attend est le signal de cohérence, pas la sortie naturelleSignaler l'arrêt avec un événementAtteindre un état cohérent sous les mêmes restrictionsSignaler que la cohérence est atteinteEntrer dans une attente infinieConfirmer la cohérence, puis TerminateThread

Figure 10 : même avec ce protocole, le travail qui établit l’état cohérent ne doit pas attendre le verrou du chargeur.

Cela ne signifie pas que n’importe quel thread en cours d’exécution peut simplement être coupé. Examinez d’abord si le thread peut être possédé hors de la DLL.

6.3 À la sortie du processus, l’idéal est de revenir sans rien faire

Au moment où DLL_PROCESS_DETACH est délivré à la sortie du processus, les autres threads se sont terminés ou ont été terminés de force, et on ne peut pas compter sur la cohérence de l’espace d’adresses. L’état des DLL et runtimes dépendants n’est pas non plus fiable, donc un nettoyage tel que libérer de la mémoire devient dangereux plutôt qu’utile. Le guidage officiel dit aussi que le gestionnaire idéal dans ce cas est vide.5

Écrivez les données qui doivent être persistées dans le propre code d’arrêt de l’application. N’utilisez pas DLL_PROCESS_DETACH comme le dernier endroit où tout peut encore être rangé.

Les prémisses du nettoyage diffèrent entre déchargement de DLL et sortie de processusLorsque seule la DLL est déchargée, le processus continue donc les ressources doivent être nettoyées, mais évitez d'attendre la sortie naturelle à l'intérieur de DllMain ; à la sortie du processus, ne comptez pas sur la cohérence des autres threads et ressources, revenez essentiellement sans rien faire, et écrivez auparavant l'état à sauvegarder dans le code d'arrêt de l'applicationDéchargement de la DLL seulementSortie du processusRaison de DLL_PROCESS_DETACH ?Le processus continue ensuiteNettoyer correctement les ressources restantesNe pas attendre la sortie naturelle à l'intérieur de DllMainL'état des autres threads et ressources est incertainRevenir essentiellement sans rien faireSauvegarder auparavant dans le code d'arrêt de l'application

Figure 11 : décidez non seulement « un nettoyage est-il nécessaire », mais si le processus continue et si les ressources utilisées pour le nettoyage sont fiables.

7. Investiguer un blocage — trouver le côté qui attend le verrou du chargeur et le côté qui le détient

7.1 Confirmer dans un dump la paire de threads qui s’attendent

Capturez un dump au moment du gel et examinez la pile de chaque thread. L’empreinte typique est une paire : un thread qui attend un verrou à l’intérieur d’une fonction du chargeur dans ntdll.dll dont le nom commence par Ldr, et un thread qui attend autre chose à l’intérieur de DllMain ou d’un initialiseur statique (dynamic initializer).

Un thread arrêté au milieu d’un appel LoadLibrary est un autre participant typique. Une fois que la relation entre le côté qui attend le verrou et le côté qui le détient en attendant autre chose se connecte, vous pouvez presque certainement conclure qu’il s’agit d’un interblocage du verrou du chargeur.

Confirmer une attente mutuelle du verrou du chargeur à partir d'un dump de blocageDans la pile de chaque thread, chercher une attente de verrou dans les fonctions Ldr et une attente à l'intérieur de DllMain ou d'un initialiseur statique, confirmer que les deux s'attendent, et si la paire typique n'apparaît pas, investiguer aussi d'autres types de blocageOuiPas visibleCapturer un dump pendant le blocageExaminer la pile de chaque threadAttente de verrou dans les fonctions LdrAttente à l'intérieur de DllMain ou d'un initialiseur statiqueLa relation d'attente mutuelle se connecte-t-elle ?Interblocage du verrou du chargeurInvestiguer aussi d'autres types de blocage

Figure 12 : ne décidez pas à partir d’une seule pile ; cherchez la paire du côté qui attend le verrou du chargeur et du côté qui le détient en attendant.

Des conditions telles que « occasionnellement au démarrage », « seulement sur une machine particulière » ou « seulement lorsqu’il est exécuté comme un service » sont aussi des indices. L’attente mutuelle se forme lorsqu’un chargement de DLL coïncide avec un démarrage ou une sortie de thread, donc les différences d’environnement et le moment d’exécution changent la façon dont elle apparaît.

7.2 Prévenir avec Application Verifier et la revue des cibles d’appel

Lancer des tests avec Application Verifier activé détecte à l’exécution les erreurs typiques de DllMain.1 En C++/CLI, n’ignorez pas l’avertissement C4747, et vérifiez les chemins indirects que l’avertissement ne peut pas détecter du point de vue de la section 4.2.

Dans la revue, cherchez des fonctions qui appellent LoadLibrary indirectement parmi le travail atteignable depuis DllMain. L’initialisation COM, certaines fonctionnalités de la CRT et les imports à chargement différé (delay-load) sont des points à vérifier. En particulier, il est facile de manquer que le premier appel d’une fonction d’import à chargement différé devient un LoadLibrary en interne. Même si le nom de la fonction vit hors de DllMain, tant que l’appelant est DllMain, la restriction demeure.

8. Résumé — ne pas regarder le nom de la fonction, mais « quand, et sous quel verrou, ça s’exécute »

Les restrictions de DllMain se comprennent toutes à partir d’un seul point : il est appelé en détenant le verrou du chargeur à l’échelle du processus. Non seulement votre propre DllMain, mais aussi l’initialisation et la terminaison statiques de la CRT et les appels indirects de C++/CLI tombent dans le même périmètre.

Commencez une revue en demandant si l’initialisation peut être rendue statique et si le reste peut être différé jusqu’après la fin du chargement. Vérifiez jusqu’au premier accès du travail différé, et si les notifications sont inutiles, envisagez DisableThreadLibraryCalls après avoir vérifié les conditions de CRT statique et de TLS statique.

Du côté de l’arrêt, séparez un déchargement de la DLL seulement d’une sortie de tout le processus. Si la DLL possède un worker, suivez le protocole signal, vérification de cohérence et terminaison, et déplacez si possible la propriété des threads du côté de l’EXE. Rapprochez DllMain à la sortie du processus du vide, et placez la sauvegarde des données dans le propre code d’arrêt de l’application.

L'ordre pour revoir DllMain et ce qu'il appelleExaminer le périmètre en incluant non seulement DllMain mais l'initialisation statique et les appels indirects, déplacer l'initialisation vers le statique ou après le chargement, concevoir l'arrêt et le nettoyage selon la raison de l'arrêt, puis confirmer avec Application Verifier et des dumpsDllMain, initialisation statique et cibles d'appelDéplacer l'initialisation vers le statique ou après le chargementVérifier aussi le premier accès du travail différéConcevoir le nettoyage selon la raison de l'arrêtConfirmer avec Verifier, avertissements et dumps

Figure 13 : suivre non seulement ce que fait l’initialisation, mais quand elle s’exécute et sa durée de vie à l’arrêt, permet de traiter les interdictions comme une seule politique de conception.

La question à poser dans un cas limite est : « ce travail est-il acceptable à exécuter en détenant le verrou du chargeur ? » Ne pas entasser dans DllMain une initialisation douteuse, et changer plutôt le moment où elle s’exécute, est le point de départ d’une conception de DLL sûre.

Articles connexes

Domaines de conseil associés

KomuraSoft LLC prend en charge l’investigation des causes profondes des blocages et interblocages au démarrage ou au chargement d’une DLL (analyse de dump), les revues de conception autour de DllMain et de l’initialisation statique, et la remédiation des wrappers C++/CLI et des DLL de plug-in vers une conception d’initialisation sûre. Vous pouvez nous consulter même au stade difficile à reproduire de « cela ne se bloque au démarrage que dans un environnement particulier ».

Références

  1. Microsoft Learn, Dynamic-Link Library Best Practices. Sur le fait que DllMain est appelé alors que le verrou du chargeur est détenu, de sorte que les fonctions qu’il peut appeler sont sévèrement restreintes ; le DllMain idéal étant un stub vide, l’initialisation étant différée autant que possible ; la recommandation d’une initialisation statique à la compilation ; ne faire que le minimum pour les échecs qui doivent être détectés tôt ; et détecter les erreurs typiques de DllMain avec Application Verifier. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7

  2. Microsoft Learn, DllMain entry point. Sur le fait de n’effectuer qu’une initialisation et une terminaison simples dans le point d’entrée ; pourquoi il ne faut pas appeler LoadLibrary / FreeLibrary (ordre de chargement circulaire et utilisation d’une DLL avant l’initialisation ou après la terminaison) ; Kernel32.dll étant garanti déjà chargé, de sorte qu’on peut l’appeler dans la plage qui ne charge pas d’autres DLL ; l’absence de liste exhaustive de fonctions sûres ; les fonctions User, Shell et COM provoquant des violations d’accès ; les notifications de DLL étant sérialisées, de sorte que la communication avec d’autres threads ou processus provoque des interblocages ; et les mêmes restrictions s’appliquant aux constructeurs et destructeurs des objets statiques lorsque la CRT est liée. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7

  3. Microsoft Learn, Initialization of Mixed Assemblies. Sur le fait de ne pas exécuter de MSIL sous le verrou du chargeur ; de ne pas compiler DllMain et son arbre d’appels en MSIL et de traiter cela via #pragma unmanaged ; l’avertissement C4747 étant émis lorsque DllMain essaie d’exécuter du MSIL directement, tandis que l’exécution indirecte à travers un autre module ne peut pas être détectée ; et les initialiseurs dynamiques des objets statiques pouvant causer le même problème. ↩ ↩2 ↩3 ↩4

  4. Microsoft Learn, DisableThreadLibraryCalls function (libloaderapi.h). Sur le fait de désactiver les notifications DLL_THREAD_ATTACH / DLL_THREAD_DETACH pour réduire le surcoût à la création et à la destruction de threads ; ne pas l’appeler depuis une DLL liée avec la CRT statique ; et l’optimisation n’étant pas effectuée lorsque le TLS statique (thread_local ou __declspec(thread)) est en vigueur. ↩ ↩2 ↩3

  5. Microsoft Learn, Dynamic-Link Library Best Practices - Best Practices for Synchronization. Sur la structure qui interbloque lorsque DllMain attend qu’un thread se termine (la notification DLL_THREAD_DETACH de la sortie de thread a besoin du verrou du chargeur) ; le protocole pour arrêter les threads au déchargement (signaler avec un événement, confirmer un état cohérent, puis terminer) ; DLL_PROCESS_DETACH à la sortie du processus, où les autres threads ont déjà été terminés de force, la cohérence de l’espace d’adresses n’est pas garantie, et le gestionnaire idéal est vide ; et créer un thread dans DllMain laissant des notifications en file avec une initialisation incomplète et causant des problèmes. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8

  6. Microsoft Learn, Dynamic-Link Library Best Practices - Deadlocks Caused by Lock Order Inversion. Sur le fait de définir une hiérarchie de verrous et d’acquérir toujours dans le même ordre ; le chargeur acquérant le verrou du chargeur avant d’appeler DllMain, de sorte que le verrou du chargeur doit siéger au sommet de la hiérarchie de verrous ; observer l’ordre d’acquisition entre les API qui prennent le verrou du chargeur indirectement, telles que GetModuleFileName, et les verrous privés ; et un exemple concret d’un interblocage par inversion d’ordre de verrous. ↩ ↩2

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

Questions fréquentes

Questions souvent posées lors d’une consultation sur le sujet de cet article.

Peut-on vraiment ne rien faire du tout dans DllMain ?
« Ne rien faire » n'est pas une hyperbole ; c'est la politique de conception officielle, et Microsoft elle-même dit que le DllMain idéal est un stub quasi vide. Ce qui est sûr se limite aux fonctions de Kernel32.dll, qui est garanti chargé au moment où DllMain s'exécute, qui ne chargent pas d'autres DLL. Vous pouvez par exemple créer des sections critiques et des mutex, et utiliser le TLS. Inversement, LoadLibrary/FreeLibrary, la synchronisation avec d'autres threads et les appels vers User32, le Shell, COM et analogues sont interdits parce qu'ils provoquent des interblocages et des violations d'accès. Pour une initialisation dont vous n'êtes pas sûr, la bonne réponse n'est pas de la faire dans DllMain, mais de la différer jusqu'à la première utilisation.
Les constructeurs des globaux C++ (objets statiques) tombent-ils aussi sous les restrictions de DllMain ?
Ils le font. Lorsque la DLL est liée avec la CRT (le runtime C++), les constructeurs et destructeurs des objets globaux et statiques s'exécutent via le point d'entrée que la CRT fournit, effectivement comme une partie de DllMain. Appeler LoadLibrary dans un constructeur, démarrer un autre thread et attendre qu'il se termine, initialiser COM, etc., portent tous le même danger que de le faire dans DllMain. Pour les objets globaux à initialisation complexe, décalez le moment d'exécution hors de DllMain : gardez un pointeur et créez l'objet au premier accès, ou utilisez un static local à une fonction.
Dois-je appeler DisableThreadLibraryCalls ?
C'est utile sous conditions. Si la DLL n'a pas besoin des notifications DLL_THREAD_ATTACH/DETACH, appeler DisableThreadLibraryCalls dans DLL_PROCESS_ATTACH arrête la notification à chaque création et destruction de thread et réduit le surcoût dans un processus qui crée des threads fréquemment. Il y a toutefois deux exceptions. Ne l'appelez pas dans une DLL liée avec la CRT statique (la CRT statique a besoin des notifications de thread). Et lorsque le TLS statique via thread_local ou __declspec(thread) est en vigueur, l'appel lui-même échoue et renvoie FALSE, donc prenez l'habitude de vérifier la valeur de retour. Utilisez-le dans une DLL typique avec la CRT liée dynamiquement, après avoir confirmé que rien ne dépend des notifications de thread.
Pourquoi une DLL C++/CLI (mixte gérée) se bloque-t-elle au démarrage ?
La cause typique est d'essayer d'exécuter du MSIL (code géré) alors que le verrou du chargeur est détenu. Dans un assemblage mixte C++/CLI, si DllMain, les fonctions qu'il appelle ou les initialiseurs dynamiques des globaux sont compilés en MSIL, l'initialisation du CLR ou le chargement d'un autre assemblage devient nécessaire sous le verrou du chargeur, et cela peut interbloquer. Le compilateur émet l'avertissement C4747 lorsque DllMain essaie d'exécuter du MSIL directement, mais il ne peut pas détecter une exécution indirecte à travers un autre module. Le remède est de compiler DllMain et son arbre d'appels en natif avec #pragma unmanaged, ou de ne pas avoir de DllMain du tout.
Puis-je nettoyer des ressources dans DLL_PROCESS_DETACH ?
La réponse diffère entre « sortie de processus » et « déchargement via FreeLibrary ». Dans DLL_PROCESS_DETACH à la sortie du processus, les autres threads ont déjà été terminés de force et il n'y a aucune garantie que l'espace d'adresses soit cohérent, donc un nettoyage tel que libérer de la mémoire est en fait dangereux, et le guidage officiel est que « le gestionnaire idéal est vide ». Écrivez les données qui doivent être persistées dans le propre code d'arrêt de l'application, et dans le gestionnaire ne faites essentiellement rien et revenez. Lors d'un déchargement via FreeLibrary, au contraire, le processus continue, donc un nettoyage complet, comme arrêter les threads et fermer les handles, est requis. Attendre qu'un thread se termine à l'intérieur de DllMain interbloque toutefois, donc vous devez suivre le protocole que la documentation officielle décrit : signaler, attendre jusqu'à un état cohérent, et faire le travail final hors de DllMain.

Profil de l’auteur

Page de présentation de l’auteur de l’article.

Go Komura

Représentant de KomuraSoft LLC

Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.

Retour au blog