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

· · Windows, DLL, Développement Windows, C++, Investigation de bugs, Multithreading, Win32 API

« L’application se bloque au démarrage, mais seulement dans un environnement particulier. » « Lorsque nous chargeons notre propre DLL, LoadLibrary ne revient parfois jamais. » « Cela n’interbloque qu’au moment du démarrage d’un service. » — Suivez assez loin des investigations de ce type et, plus souvent qu’autrement, vous arrivez au même endroit. Le code d’initialisation de la DLL — c’est-à-dire DllMain.

La documentation de Microsoft met en garde sur DllMain sur un ton inhabituellement fort. N’appelez pas LoadLibrary. Ne synchronisez pas avec d’autres threads. N’appelez pas les fonctions User, Shell ou COM. Le DllMain idéal est un stub vide — pourquoi le langage est-il aussi fort ? La raison se concentre dans un seul mécanisme interne, le verrou du chargeur. Destiné aux développeurs qui écrivent des DLL, des plug-ins et des wrappers C++/CLI sous Windows, cet article explique, à partir des sources primaires, comment le verrou du chargeur fonctionne, la structure qui fait qu’un interblocage tient, et la conception sûre et la procédure d’investigation.

1. La conclusion, d’abord

  • DllMain est appelé en détenant le verrou du chargeur, un verrou partagé dont il n’existe exactement un par processus. Donc appeler, depuis DllMain, un travail qui essaie de prendre le verrou du chargeur (directement ou indirectement) crée la possibilité d’un interblocage, ou d’un plantage en touchant une DLL qui n’a pas encore été initialisée.1
  • Appeler LoadLibrary / FreeLibrary est interdit. Cela crée une dépendance circulaire d’ordre de chargement et peut faire s’exécuter du code d’initialisation contre une DLL dont la propre initialisation n’a pas encore tourné.2
  • Synchroniser avec d’autres threads est aussi interdit. Les notifications de DLL sont sérialisées, donc attendre à l’intérieur de DllMain qu’un thread démarre ou se termine laisse ce thread lui-même arrêté en attendant le verrou du chargeur, et vous interbloquez.23
  • Ce que vous pouvez appeler en toute sécurité est, en pratique, seulement un sous-ensemble de Kernel32.dll. Et la documentation officielle indique clairement qu’« une liste complète des fonctions sûres n’existe pas ». Les fonctions User, Shell et COM chargent d’autres composants et provoquent des violations d’accès.2
  • Dans une DLL liée avec la CRT, les mêmes restrictions s’appliquent aux constructeurs et destructeurs des globaux. Ils s’exécutent comme une partie de facto de DllMain.2
  • La conception correcte est « différer ». Faites l’initialisation que vous pouvez à la compilation (statiquement) ; différez ce que vous ne pouvez pas jusqu’à la première utilisation. C’est la meilleure pratique officielle.1
  • Les DLL mixtes C++/CLI sont particulièrement dangereuses. Pour éviter d’exécuter du MSIL sous le verrou du chargeur, DllMain et son arbre d’appels doivent être compilés en natif.4

2. Quand et comment DllMain est appelé

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.

Notification Moment
DLL_PROCESS_ATTACH Lorsque la DLL est chargée dans le processus
DLL_THREAD_ATTACH Lorsqu’un nouveau thread est démarré 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

Deux faits sont faciles à manquer. Premier, chaque fois qu’un seul thread est créé, le DllMain de chaque DLL déjà chargée est appelé avec DLL_THREAD_ATTACH. Autrement dit DllMain n’est pas « quelque chose qui s’exécute une fois lorsque ma DLL est chargée » ; c’est du code qui continue d’être appelé pour l’activité de threads du processus. Si vous n’en avez pas besoin, vous pouvez l’arrêter en appelant DisableThreadLibraryCalls à l’intérieur de DLL_PROCESS_ATTACH (ne l’appelez pas depuis une DLL liée avec la CRT statique).5

Deuxième, 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, comme une partie de DllMain.2 Même si vous pensez « notre DllMain est vide, donc nous sommes en sécurité », un objet global dont l’initialisation est élaborée équivaut à exécuter ce travail dans DllMain.

Les quatre moments auxquels DllMain est appeléDLL_PROCESS_ATTACH s'exécute au chargement de la DLL ; DLL_THREAD_ATTACH et DETACH s'exécutent sur chaque DLL déjà chargée à chaque démarrage et sortie de thread dans le processus ; DLL_PROCESS_DETACH s'exécute au déchargement ou à la sortie du processus ; et les constructeurs des objets statiques s'exécutent aussi à l'intérieur via la CRTChargement de la DLLDLL_PROCESS_ATTACHDLL_THREAD_ATTACH (à chaque démarrage de thread)DLL_THREAD_DETACH (à chaque sortie de thread)DLL_PROCESS_DETACH (au déchargement ou à la sortie)La construction des objets statiques s'exécute aussi ici

Figure 1 : DllMain est appelé non seulement au chargement mais à chaque démarrage et sortie de thread, et l’initialisation des objets statiques s’exécute aussi comme une partie de cela.

3. Le verrou du chargeur — un verrou qui sérialise chaque notification

Pourquoi les restrictions sur DllMain seul sont-elles aussi sévères ? La réponse est dans la structure du chargeur.

Pour garder cohérente une série d’opérations — chargement de DLL, déchargement et les diverses notifications — le chargeur de l’OS sérialise le travail avec un seul verrou du chargeur par processus. Et le point important est que DllMain est appelé alors que ce verrou du chargeur est détenu.1 Aussi longtemps que vous êtes à l’intérieur de DllMain, chaque autre chargement de DLL dans ce processus, et chaque notification de début de thread, attend que ce verrou soit libéré.

De cette structure, les raisons des interdictions suivent les unes après les autres.

  • Vous ne devez pas appeler LoadLibrary parce que cela crée une réentrance du verrou du chargeur, ou une dépendance circulaire d’ordre de chargement. Cela peut aussi aboutir à appeler une fonction sur une DLL dont l’initialisation n’est pas encore terminée.2
  • Synchroniser avec d’autres threads est dangereux parce que le thread que vous attendez a des moments où il a besoin du verrou du chargeur (notifications au démarrage et à la sortie, appels aux API de la famille GetModuleHandle, etc.). Vous détenez le verrou du chargeur et attendez l’autre côté ; l’autre côté attend le verrou du chargeur — l’inversion classique d’ordre de verrous.6
  • Les fonctions User, Shell et COM sont dangereuses parce qu’elles chargent d’autres composants système en interne. Vous touchez un composant avant qu’il ne soit initialisé, ou après qu’il a été démonté, et vous obtenez une violation d’accès.2
Pourquoi attendre un thread à l'intérieur de DllMain interbloqueDllMain, détenant le verrou du chargeur, attend qu'un thread worker se termine, mais le worker qui essaie de se terminer attend que le verrou du chargeur soit libéré afin de recevoir DLL_THREAD_DETACH, donc ils s'attendent mutuellement et interbloquentWorkerDllMainChargeur(détient le verrou)WorkerDllMainChargeur(détient le verrou)La notification de sortie a besoin du verrouDllMain détient le verrou, W attendDLL_PROCESS_DETACHDemander la sortie et attendreTerminer le travail, puis sortir

Figure 2 : « DllMain attend qu’un thread se termine » est un interblocage structurel, parce que la sortie de thread elle-même a besoin du verrou du chargeur.

Le point est que ce n’est pas le genre de chose qui « arrive si vous n’avez pas de chance » ; c’est structurellement garanti de tenir. La documentation vous dit de traiter le verrou du chargeur comme le sommet de la hiérarchie de verrous que l’application définit (celui pris en premier). À l’intérieur de DllMain vous détenez déjà ce verrou de premier niveau, donc tout acte de continuer de là à attendre autre chose est dangereux — c’est une façon utile de s’en souvenir.6

Inversion d'ordre de verrous entre le verrou du chargeur et un verrou privéDllMain, détenant le verrou du chargeur, va prendre un verrou privé, tandis qu'un worker, détenant ce verrou privé, va prendre le verrou du chargeur pour GetModuleHandle ou analogue, donc l'ordre d'acquisition s'inverse et ils interbloquentDllMain : détient le verrou du chargeurVa prendre le verrou privé GWorker : détient le verrou privé GVa prendre le verrou du chargeurInterblocage par ordre d'acquisition inverséGetModuleHandle et analogues l'exigent en interne

Figure 3 : même une API anodine telle que GetModuleHandle exige le verrou du chargeur en interne, donc une inversion d’ordre avec un verrou privé peut tenir.

Aussi, appeler CreateThread depuis l’intérieur de DllMain lui-même n’est pas recommandé. Le thread créé a besoin du verrou du chargeur pour traiter la notification DLL_THREAD_ATTACH, donc il ne peut pas commencer à s’exécuter jusqu’à ce que le DllMain actuellement en cours revienne et libère le verrou. Donc attendre à l’intérieur de DllMain que ce thread démarre ou se termine est un interblocage immédiat. Il y a aussi un problème de durée de vie — si, après que DllMain revient, la DLL est déchargée alors qu’un thread qui n’a pas encore commencé à s’exécuter est encore laissé derrière, l’adresse de démarrage du thread pointe encore vers du code déjà libéré et vous plantez.3

4. Deux mines sur lesquelles les développeurs C++ marchent facilement

Mine 1 : l’initialisation dynamique des objets globaux. Comme le disait le chapitre 2, les constructeurs des objets statiques s’exécutent sous les restrictions de DllMain. Lire un fichier de configuration, mettre debout une installation de journalisation, initialiser COM, démarrer un thread — le moment où vous mettez dans une DLL un global dont le constructeur fait ce genre de travail, vous exécutez « des choses que vous ne devez pas faire dans DllMain ». L’initialisation constante qui est fixée à la compilation (tout ce que vous pouvez rendre constexpr) est sûre ; l’initialisation qui implique un appel de fonction doit être différée.

Le chemin par lequel initialiser un objet global devient une mineLe verrou du chargeur est pris au chargement de la DLL, et les constructeurs des objets globaux s'exécutent via le point d'entrée CRT, donc LoadLibrary, la synchronisation de threads et l'initialisation COM à l'intérieur de ces constructeurs sont des exécutions d'interdictions de DllMainChargement de la DLL (verrou du chargeur acquis)Point d'entrée CRTConstructeur d'un objet globalTravail équivalent à LoadLibraryDémarrer un thread et attendre qu'il se termineUtiliser COM ou User32Tout cela tombe sous les interdictions de DllMain

Figure 4 : même « DllMain est vide, donc nous sommes en sécurité » ravive le même danger dès que vous avez un global dont l’initialisation est élaborée.

Mine 2 : C++/CLI (assemblages mixtes). Dans une configuration qui enveloppe une DLL native avec C++/CLI (la forme couverte dans l’article sur les wrappers), il y a un danger d’exécuter du MSIL (code géré) sous le verrou du chargeur. Exécuter du MSIL peut déclencher l’initialisation du CLR ou le chargement d’un autre assemblage. Le compilateur émet l’avertissement C4747 sur le code où DllMain exécute du MSIL directement, mais il ne peut pas détecter une exécution indirecte à travers une fonction d’un autre module. Compilez DllMain et les fonctions appelées depuis celui-ci en natif avec #pragma unmanaged, ou utilisez une configuration qui n’a pas de DllMain du tout.4

Si l'exécution de MSIL sous le verrou du chargeur peut être détectéeLe code où DllMain exécute du MSIL directement peut être détecté par le compilateur avec l'avertissement C4747, mais l'exécution indirecte à travers une fonction d'un autre module ne le peut pas, donc vous devez l'empêcher en revoyant l'arbre d'appels et en exigeant une compilation nativeAppels depuis DllMainExécuter du MSIL directementExécuter via un autre moduleDétectable avec l'avertissement C4747Le compilateur ne peut pas le détecterEmpêcher par revue et #pragma unmanaged

Figure 5 : C4747 ne vous protège que contre l’exécution directe. Les chemins indirects ne peuvent être attrapés que par la revue.

5. La conception correcte — faire de « différer » la politique par défaut

La recommandation officielle de meilleure pratique est claire.1

  1. Terminez l’initialisation que vous pouvez à la compilation (statiquement). Demandez d’abord si une initialisation dynamique peut être remplacée par une statique.
  2. Différez le reste jusqu’à la première utilisation. Tant que la première utilisation se produit depuis une API ordinaire appelée après que la DLL a fini de se charger, l’initialisation s’exécute hors du verrou du chargeur et vous pouvez utiliser en toute sécurité presque toute l’API Windows. Pour l’exclusion au premier accès vous pouvez utiliser INIT_ONCE (initialisation unique) ou les magic statics C++ (statics locaux à une fonction). Le report n’est toutefois pas une panacée — si ce premier accès lui-même est fait depuis DllMain ou un initialiseur statique, l’initialiseur s’exécute encore sous le verrou du chargeur et vous êtes de nouveau sous les mêmes restrictions.
  3. Ne faites d’exception que pour les échecs que vous devez détecter tôt. Vous pouvez avoir une exigence qu’un fichier de configuration cassé fasse échouer le chargement lui-même. Même alors, tenez-vous au minimum de « essayer et échouer immédiatement ».
  4. Envisagez DisableThreadLibraryCalls dans DLL_PROCESS_ATTACH. Si la DLL n’utilise pas les notifications de thread, vous pouvez supprimer le coût de notification lui-même (sauf lorsque vous utilisez la CRT statique ou le TLS statique).5
  5. Inspectez avec Application Verifier. Beaucoup des appels dangereux à l’intérieur de DllMain sont des appels qu’Application Verifier détectera à l’exécution.1
Guidage de conception pour l'initialisation d'une DLLEnvisagez d'abord si l'initialisation peut être une initialisation statique à la compilation ; sinon, le défaut est de différer à la première utilisation, et de ne laisser dans DllMain que le minimum qui doit être détecté tôt comme un échec de chargementouinonnonouiPeut-elle être décidée à 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 DllMainExclure avec INIT_ONCE ou un static local à une fonction

Figure 6 : l’ordre de décision est « peut-elle être statique → peut-elle être différée », et ce que vous laissez dans DllMain n’est que le minimum qui doit être détecté tôt.

Que d’appliquer DisableThreadLibraryCalls peut se décider mécaniquement avec la branche suivante.

S'il faut appeler DisableThreadLibraryCallsNe l'appelez pas depuis une DLL liée avec la CRT statique ; si le TLS statique est en vigueur l'appel lui-même échoue donc vous ne l'appelez pas ; si ni l'un ni l'autre ne s'applique et que la DLL n'utilise pas les notifications de thread, appelez-le dans DLL_PROCESS_ATTACH, en vérifiant la valeur de retour, pour couper le coût de notificationouinonouinonnonouiLiée avec la CRT statique ?Il ne faut pas l'appelerUtilise le TLS statique ?L'appel échoue de toute façon (FALSE)Les notifications de thread sont-elles nécessaires ?L'appeler dans ATTACH (vérifier la valeur de retour)Ne pas l'appeler ; gérer les notifications

Figure 7 : les trois conditions de CRT statique, TLS statique, et si les notifications sont nécessaires, décident de façon unique si vous devez l’appeler.

Pour arrêter les threads au déchargement, la documentation officielle donne un protocole concret. Plutôt que « d’attendre » que les threads worker se terminent dans DLL_PROCESS_DETACH (sur un déchargement via FreeLibrary), la forme est (1) signaler la sortie avec un événement, (2) le côté thread replie son travail jusqu’à un état cohérent, signale en retour, et entre dans une attente infinie, (3) le côté DllMain confirme l’état cohérent puis replie le thread avec TerminateThread.3 Cela a l’air rude, mais c’est documenté comme la réponse réaliste à l’intérieur de la contrainte « vous ne devez pas attendre la sortie naturelle d’un thread à l’intérieur de DllMain ».

Protocole pour arrêter un thread au déchargementDllMain signale au thread worker de sortir avec un événement ; le worker replie son travail jusqu'à un état cohérent, signale en retour, et entre dans une attente infinie ; DllMain confirme l'état cohérent puis termine le threadThread workerDllMain (traitement DETACH)Thread workerDllMain (traitement DETACH)Pas d'attente de sortie naturelle, donc pas d'interblocageSignaler la sortie avec un événementReplier le travail jusqu'à un état cohérentSignaler la cohérence terminée et attendre indéfinimentTerminer avec TerminateThread

Figure 8 : au lieu d’« attendre une sortie naturelle », « attendre un signal de cohérence puis le couper » évite une collision avec le verrou du chargeur.

En matière de premiers principes, la conception la plus sûre est d’éviter de posséder des threads dans une DLL qui peut être déchargée, et de garder la propriété des threads du côté de l’EXE.

DLL_PROCESS_DETACH à la sortie du processus est l’opposé : ne rien faire et revenir est l’idéal. À ce point chaque autre thread a déjà été terminé de force, et vous ne pouvez pas non plus vous fier à l’état des DLL dépendantes ou du runtime. Un travail élaboré ici ne fait que provoquer des interblocages et des plantages. Les données qui doivent être persistées doivent être écrites dans le propre chemin d’arrêt de l’application ; ne dépendez pas de cette notification.3

6. Comment investiguer lorsque vous le rencontrez

Les blocages du verrou du chargeur ont une empreinte reconnaissable.

Regardez les piles dans un dump de blocage. Prenez un dump du moment figé et inspectez la pile de chaque thread. Si vous trouvez une paire d’un thread qui attend sur un verrou à l’intérieur des fonctions du chargeur de ntdll.dll (la famille dont les noms commencent par Ldr) et d’un thread qui attend autre chose à l’intérieur de DllMain ou d’un initialiseur statique (dynamic initializer), vous êtes presque certain. Un thread arrêté au milieu d’un appel LoadLibrary est un autre personnage typique.

L'empreinte d'un blocage du verrou du chargeurDans un dump de blocage, si vous trouvez à la fois un thread qui attend sur un verrou à l'intérieur des fonctions du chargeur ntdll et un thread qui attend autre chose à l'intérieur de DllMain ou d'un initialiseur statique, vous pouvez le traiter comme un interblocage du verrou du chargeur avec une quasi-certitudeouinonDump de blocageThread en attente sur un verrou dans les fonctions LdrThread en attente à l'intérieur de DllMain ou d'un initialiseur statiqueLes deux présents ?Presque certainement un interblocage du verrou du chargeurInvestiguer comme un blocage d'un autre type

Figure 9 : les blocages du verrou du chargeur ont l’empreinte reconnaissable « attente dans Ldr + attente à l’intérieur de DllMain ».

Suspectez le caractère « dépendant du moment ». Un interblocage du verrou du chargeur ne tient qu’au moment où un chargement de DLL coïncide avec un démarrage ou une sortie de thread. Des conditions de reproduction telles que « occasionnellement au démarrage », « seulement sur une machine particulière » et « seulement lorsqu’il est exécuté comme un service » sont des signes de ce type de problème.

Exécutez une inspection préventive. Activez Application Verifier et lancez vos tests, et vous pouvez détecter les appels dangereux à l’intérieur de DllMain à l’exécution.1 Pour C++/CLI, n’ignorez pas l’avertissement C4747 ; dans les revues des fonctions atteignables depuis DllMain, ajoutez l’angle des « fonctions qui appellent LoadLibrary indirectement » (initialisation COM, certaines fonctionnalités de la CRT, imports à chargement différé, etc.) à la liste de contrôle de revue, et vous attraperez les accidents avant de les livrer. Le premier appel d’un import à chargement différé devenant LoadLibrary en interne est un point facile à manquer.

7. Résumé

  • DllMain est appelé en détenant le verrou du chargeur (un par processus, le verrou qui sérialise chaque notification de DLL). Chaque restriction suit de cela.
  • Le cœur des interdictions est « n’appelez pas LoadLibrary / FreeLibrary », « ne synchronisez pas avec d’autres threads », et « n’appelez pas de fonctions qui dépendent d’une DLL autre que Kernel32 ». Les constructeurs et destructeurs des objets statiques qui s’exécutent via la CRT tombent sous les mêmes restrictions.
  • La politique de conception de base est le report. Rendez statique l’initialisation que vous pouvez rendre statique ; différez le reste à la première utilisation. Utilisez DisableThreadLibraryCalls et Application Verifier.
  • Arrêter les threads au déchargement suit le protocole officiel (signaler → confirmer la cohérence → terminer). DLL_PROCESS_DETACH à la sortie du processus est idéalement vide.
  • En C++/CLI, exécuter du MSIL sous le verrou du chargeur est une mine à part. Exigez une compilation native de l’arbre d’appels de DllMain.

Les restrictions de DllMain ont l’air, au premier abord, d’une liste déraisonnable d’interdictions. Mais une fois que vous tenez le point unique qu’« il est appelé en détenant le verrou du chargeur, le verrou de premier niveau », chaque interdiction est une reformulation du même principe. Souvenez-vous-en comme d’un principe et, lorsque vous rencontrez un cas limite qui n’est pas dans la documentation, vous devriez encore pouvoir poser la bonne question : « Est-ce un travail que j’ai le droit de faire en détenant le verrou ? »

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 que vous pouvez appeler sont sévèrement restreintes ; le DllMain idéal étant un stub vide et 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

  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 que vous pouvez 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, Dynamic-Link Library Best Practices - Best Practices for Synchronization. Sur la structure qui interbloque si vous attendez qu’un thread se termine à l’intérieur de DllMain (la notification DLL_THREAD_DETACH de la sortie de thread a besoin du verrou du chargeur) ; le protocole pour arrêter un thread au déchargement (signaler avec un événement, confirmer un état cohérent, puis terminer) ; DLL_PROCESS_DETACH à la sortie du processus ayant les autres threads déjà terminés de force et aucune garantie de cohérence de l’espace d’adresses, de sorte que 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

  4. 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, mais l’exécution indirecte à travers un autre module n’étant pas détectable ; et les initialiseurs dynamiques des objets statiques pouvant causer le même problème.  2

  5. 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

  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 posture de conception officielle, et Microsoft elle-même dit que le DllMain idéal est un stub quasi vide. Ce qui est sûr, c'est un sous-ensemble des fonctions de Kernel32.dll — Kernel32 est garanti chargé au moment où DllMain s'exécute — dans la plage qui ne charge pas d'autres DLL. Créer une section critique ou un mutex, et utiliser le TLS, sont des exemples de ce que vous pouvez faire. Inversement, LoadLibrary/FreeLibrary, synchroniser avec d'autres threads, et appeler des fonctions de User32, du Shell, de COM et analogues sont interdits parce qu'ils provoquent des interblocages et des violations d'accès. Une initialisation dont vous n'êtes pas sûr ne doit pas être faite dans DllMain ; différez-la 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, comme une partie de facto de DllMain. Cela signifie qu'appeler LoadLibrary depuis un constructeur, démarrer un autre thread et attendre qu'il se termine, initialiser COM, et analogues, portent tous le même danger que de faire ces choses dans DllMain. Pour un objet global dont l'initialisation n'est pas triviale, gardez un pointeur et construisez-le au premier accès, ou utilisez un static local à une fonction, afin que le travail s'exécute hors de DllMain.
Dois-je appeler DisableThreadLibraryCalls ?
Conditionnellement, oui. Si la DLL n'a pas besoin des notifications DLL_THREAD_ATTACH/DETACH, appeler DisableThreadLibraryCalls dans DLL_PROCESS_ATTACH arrête les notifications par création de thread et par sortie de thread et réduit le surcoût dans un processus qui crée des threads fréquemment. Il y a deux exceptions. Ne l'appelez pas depuis une DLL liée avec la CRT statique (la CRT statique a besoin des notifications de thread). Et si 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 sur une DLL typique qui utilise 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 appelées depuis celui-ci, ou les initialiseurs dynamiques des globaux sont compilés en MSIL, l'initialisation du CLR ou le chargement d'un autre assemblage peut être requis sous le verrou du chargeur, et cela peut interbloquer. Le compilateur émet l'avertissement C4747 lorsque DllMain lui-même essaie d'exécuter du MSIL directement, mais il ne peut pas détecter une exécution indirecte à travers un autre module. L'atténuation consiste à compiler DllMain et son arbre d'appels en natif avec #pragma unmanaged — ou à ne pas avoir de DllMain du tout.
Puis-je nettoyer des ressources dans DLL_PROCESS_DETACH ?
La réponse change entre « sortie de processus » et « déchargement via FreeLibrary ». Sur DLL_PROCESS_DETACH à la sortie du processus, les autres threads ont déjà été terminés, et il n'y a aucune garantie que l'espace d'adresses soit encore cohérent, donc un nettoyage tel que libérer de la mémoire est en fait dangereux ; le guidage officiel est que « le gestionnaire idéal est vide ». Écrivez toute donnée qui doit être persistée dans le propre chemin d'arrêt de l'application, et ici essentiellement ne faites rien et revenez. Sur un déchargement via FreeLibrary, le processus continue, donc vous avez besoin d'un nettoyage complet — arrêter les threads, fermer les handles, etc. Attendre qu'un thread se termine à l'intérieur de DllMain interbloque toutefois, donc vous devez suivre le protocole officiel : signaler, attendre jusqu'à un état cohérent, et terminer le travail 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