Initialisation d'assemblys mixtes

Dans Visual C++ .NET et Visual C++ 2003, les DLL compilées avec l'option du compilateur /clr pouvaient se bloquer de façon non déterministe lors du chargement ; ce problème a été appelé le problème de chargement de DLL mixtes ou de verrouillage du chargeur. Le non-déterminisme a été pratiquement supprimé du processus de chargement de DLL mixtes. Toutefois, il reste quelques scénarios dans lesquels le verrouillage du chargeur peut se produire (de façon déterministe). Pour plus d'informations sur ce problème, consultez « Mixed DLL Loading Problem » dans MSDN Library.

Le code dans DllMain ne doit pas accéder au CLR. Cela signifie que DllMain ne doit pas appeler de fonctions managées, que ce soit directement ou indirectement ; aucun code managé ne doit être déclaré ou être implémenté dans DllMain ; par ailleurs, aucune opération de garbage collection ou de chargement de bibliothèque automatique ne doit avoir lieu dans DllMain.

Notes

Visual C++ 2003 fournit _vcclrit.h afin de faciliter l'initialisation des DLL tout en réduisant les risques d'interblocage. L'utilisation de _vcclrit.h n'est plus nécessaire et génère des avertissements de désapprobation pendant la compilation. La stratégie recommandée consiste à supprimer les dépendances sur ce fichier à l'aide des étapes décrites dans How To: Remove Dependency on _vcclrit.h. Les solutions moins idéales consistent à supprimer les avertissements en définissant _CRT_VCCLRIT_NO_DEPRECATE avant d'inclure _vcclrit.h ou d'ignorer simplement les avertissements de désapprobation.

Causes du verrouillage du chargeur

Avec l'introduction de la plateforme .NET, il existe deux mécanismes distincts pour charger un module d'exécution (EXE ou DLL) : un pour Windows qui est utilisé pour les modules non managés et un pour le Common Language Runtime (CLR) .NET qui charge les assemblys .NET. Le problème du chargement de DLL mixtes se concentre autour du chargeur du système d'exploitation Microsoft Windows.

Lorsqu'un assembly qui contient uniquement des constructions .NET est chargé dans un processus, le chargeur du CLR peut exécuter toutes les tâches de chargement et d'initialisation nécessaires lui-même. Toutefois, dans la mesure où les assemblys mixtes peuvent contenir du code natif et des données, le chargeur Windows doit aussi être utilisé.

Le chargeur Windows garantit qu'aucun code ne peut accéder au code ou aux données qui se trouvent dans cette DLL avant qu'elle n'ait été initialisée, et qu'aucun code ne peut charger de façon redondante la DLL pendant qu'elle est partiellement initialisée. Pour cela, le chargeur Windows utilise une section critique de processus global (souvent appelée « verrouillage du chargeur ») qui empêche l'accès non sécurisé pendant l'initialisation du module. En conséquence, le processus de chargement est vulnérable à de nombreux scénarios d'interblocage classiques. Pour les assemblys mixtes, les deux scénarios suivants augmentent le risque d'interblocage :

  • En premier lieu, si les utilisateurs tentent d'exécuter des fonctions compilées en langage MSIL (Microsoft Intermediate Language) lorsque le verrouillage du chargeur est maintenu (à partir de DllMain ou dans les initialiseurs statiques, par exemple), cela peut provoquer un interblocage. Considérons le cas dans lequel la fonction MSIL fait référence à un type situé dans un assembly qui n'a pas été chargé. Le CLR tentera de charger automatiquement cet assembly, ce qui peut nécessiter le blocage du chargeur Windows sur le verrouillage du chargeur. Dans la mesure où le verrouillage du chargeur est déjà maintenu par du code précédemment dans la séquence d'appel, un interblocage en résulte. Toutefois, l'exécution du langage MSIL lors du verrouillage du chargeur ne garantit pas qu'un interblocage se produira, ce qui rend ce scénario difficile à diagnostiquer et à résoudre. Dans certaines circonstances, par exemple lorsque la DLL du type référencé ne contient pas de constructions natives et qu'aucune de ses dépendances ne contient de constructions natives, le chargeur Windows ne doit pas charger l'assembly .NET du type référencé. En outre, l'assembly requis ou ses dépendances natives/.NET mixtes peuvent avoir déjà été chargés par un autre code. Par conséquent, l'interblocage peut être difficile à prédire et peut varier selon la configuration de l'ordinateur cible.

  • Ensuite, lors du chargement de DLL dans les versions 1.0 et 1.1 du .NET Framework, le CLR supposait que le verrouillage du chargeur n'était pas maintenu et exécutait plusieurs actions qui n'étaient pas valides lors du verrouillage du chargeur. Supposer que le verrouillage du chargeur n'est pas maintenu est une hypothèse valide pour les DLL exclusivement .NET, mais, comme les DLL mixtes exécutent des routines d'initialisation natives, elles requièrent le chargeur Windows natif et par conséquent le verrouillage du chargeur. Par conséquent, même si le développeur n'essayait pas d'exécuter des fonctions MSIL pendant l'initialisation de la DLL, il y avait encore une petite possibilité d'interblocage non déterministe avec les versions 1.0 et 1.1 du .NET Framework.

Le non-déterminisme a été entièrement supprimé du processus de chargement de DLL mixtes. ce qui a été fait grâce aux modifications suivantes :

  • Le CLR ne fait plus de fausses hypothèses lors du chargement de DLL mixtes.

  • L'initialisation non managée et managée est exécutée en deux étapes distinctes. L'initialisation non managée a lieu en premier (via DllMain), et l'initialisation managée a lieu après, par l'intermédiaire d'une construction prise en charge par le .NET et appelée .cctor. Cette dernière opération est complètement transparente pour l'utilisateur sauf si /Zl ou /NODEFAULTLIB est utilisé. Pour plus d'informations, consultez /NODEFAULTLIB (Ignorer les bibliothèques) et /Zl (Omettre le nom de la bibliothèque par défaut).

Le verrouillage du chargeur peut encore se produire, mais se produit désormais de façon déterministe et est détecté. Si DllMain contient des instructions MSIL, le compilateur génère l'avertissement suivant : Avertissement du compilateur (niveau 1) C4747. En outre, le CRT ou le CLR essaie de détecter et de rapporter les tentatives d'exécution du MSIL lors du verrouillage du chargeur. La détection du CRT se traduit par le diagnostic du runtime C Run-Time Error R6033.

Le reste de ce document décrit les autres scénarios pour lesquels du langage MSIL peut s'exécuter lors du verrouillage du chargeur, les solutions au problème pour chacun des scénarios et les techniques de débogage.

Scénarios et solutions de contournement

Il existe plusieurs situations où le code utilisateur peut exécuter des instructions MSIL lors du verrouillage du chargeur. Le développeur doit s'assurer que l'implémentation du code utilisateur n'essaie pas d'exécuter des instructions MSIL dans chacun de ces cas. Les sous-sections suivantes décrivent toutes les possibilités tout en indiquant comment résoudre les problèmes dans les cas les plus courants.

  • DllMain

  • Initialiseurs statiques

  • Fonctions fournies par l'utilisateur affectant le démarrage

  • Paramètres régionaux personnalisés

DllMain

La fonction DllMain est un point d'entrée défini par l'utilisateur pour une DLL. Sauf indication contraire de l'utilisateur, DllMain est appelée chaque fois qu'un processus ou qu'un thread s'attache à la DLL conteneur ou s'en détache. Dans la mesure où cet appel peut se produire alors que le verrouillage du chargeur est maintenu, aucune fonction DllMain fournie par l'utilisateur ne doit être compilée en langage MSIL. En outre, aucune fonction dans l'arborescence des appels associée à une racine DllMain ne peut être compilée en langage MSIL. Pour résoudre les problèmes à cet emplacement, le bloc de code qui définit DllMain doit être modifié avec #pragma unmanaged. Il en va de même pour chaque fonction que DllMain appelle.

Dans les cas où ces fonctions doivent appeler une fonction qui nécessite une implémentation MSIL pour d'autres contextes appelants, une stratégie de duplication peut être utilisée lorsqu'une version .NET et une version native de la même fonction sont créées.

Sinon, si DllMain n'est pas nécessaire ou qu'il n'est pas nécessaire de l'exécuter lors du verrouillage du chargeur, l'implémentation de DllMain fournie par l'utilisateur peut être supprimée, ce qui élimine le problème.

Si DllMain tente d'exécuter du langage MSIL directement, vous obtiendrez l'avertissement suivant : Avertissement du compilateur (niveau 1) C4747. Toutefois, le compilateur ne peut pas détecter les cas où DllMain appelle une fonction dans un autre module qui, à son tour, tente d'exécuter du langage MSIL.

Consultez « Obstacles au Diagnostic » pour plus d'informations sur ce scénario.

Initialisation d'objets statiques

L'initialisation d'objets statiques peut entraîner un interblocage si un initialiseur dynamique est requis. Pour les cas simples, par exemple lorsqu'une variable statique est simplement assignée à une valeur connue au moment de la compilation, aucune initialisation dynamique n'est requise ; il n'y a donc aucun risque d'interblocage. Toutefois, les variables statiques initialisées par les appels de fonction, les appels de constructeur ou les expressions qui ne peuvent pas être évaluées au moment de la compilation requièrent tous une exécution de code pendant l'initialisation de module.

Le code suivant montre des exemples d'initialiseurs statiques qui requièrent une initialisation dynamique : un appel de fonction, une construction d'objet et une initialisation de pointeur. (Ces exemples ne sont pas statiques, mais sont supposés être définis dans la portée globale, ce qui a le même effet.)

// dynamic initializer function generated
int a = init();
CObject o(arg1, arg2);  
CObject* op = new CObject(arg1, arg2);

Ce risque d'interblocage varie selon que le module conteneur est compilé avec /clr et que MSIL est exécuté. Plus particulièrement, si la variable statique est compilée sans /clr (ou réside dans un bloc #pragma unmanaged), et si l'initialiseur dynamique requis pour l'initialiser se traduit par l'exécution d'instructions MSIL, un interblocage peut se produire. Ceci est dû au fait que l'initialisation de variables statiques est exécutée par DllMain pour les modules compilés sans /clr. En revanche, les variables statiques compilées avec /clr sont initialisées par le .cctor, une fois que l'étape d'initialisation non managée est terminée et que le verrouillage du chargeur a été désactivé.

Il existe de nombreuses solutions à l'interblocage provoqué par l'initialisation dynamique de variables statiques (classées globalement selon le délai nécessaire pour résoudre le problème) :

  • Le fichier source contenant la variable statique peut être compilé avec /clr.

  • Toutes les fonctions appelées par la variable statique peuvent être compilées en code natif à l'aide de la directive #pragma unmanaged.

  • Clonez manuellement le code dont dépend la variable statique, en fournissant à la fois une version .NET et une version native avec des noms différents. Les développeurs peuvent ensuite appeler la version native à partir d'initialiseurs statiques natifs et appeler la version .NET ailleurs.

Fonctions fournies par l'utilisateur affectant le démarrage

Il existe plusieurs fonctions fournies par utilisateur dont dépendent les bibliothèques pour l'initialisation au démarrage. Par exemple, lors de la surcharge globale d'opérateurs en C++, comme les opérateurs new et delete, les versions fournies par l'utilisateur sont utilisées partout, y compris pendant l'initialisation et la destruction STL. En conséquence, STL et les initialiseurs statiques fournis par l'utilisateur appelleront les versions fournies par l'utilisateur de ces opérateurs.

Si les versions fournies par l'utilisateur sont compilées en langage MSIL, ces initialiseurs tenteront alors d'exécuter des instructions MSIL pendant que le verrouillage du chargeur est maintenu. Un malloc fourni par l'utilisateur a les mêmes conséquences. Pour résoudre ce problème, n'importe laquelle de ces surcharges ou définitions fournies par l'utilisateur doit être implémentée sous la forme de code natif à l'aide de la directive #pragma unmanaged.

Consultez « Obstacles au Diagnostic » pour plus d'informations sur ce scénario.

Paramètres régionaux personnalisés

Si l'utilisateur fournit des paramètres régionaux globaux personnalisés, ceux-ci seront utilisés pour initialiser tous les futurs flux d'E/S, y compris ceux qui sont initialisés de façon statique. Si cet objet de paramètres régionaux globaux est compilé en langage MSIL, les fonctions membres d'objets de paramètres régionaux compilées en langage MSIL peuvent être appelées pendant que le verrouillage du chargeur est maintenu.

Il existe trois possibilités pour résoudre ce problème :

Les fichiers sources contenant toutes les définitions de flux d'E/S globales peuvent être compilés à l'aide de l'option /clr. Cela empêchera l'exécution de leurs initialiseurs statiques lors du verrouillage du chargeur.

Les définitions de fonctions personnalisées de paramètres régionaux peuvent être compilées en code natif en utilisant la directive #pragma unmanaged.

Abstenez-vous de définir les paramètres régionaux personnalisés en tant que paramètres régionaux globaux tant que le verrouillage du chargeur n'a pas été libéré. Configurez ensuite de façon explicite les flux d'E/S créés pendant l'initialisation avec les paramètres régionaux personnalisés.

Obstacles au diagnostic

Dans certains cas, il est difficile de détecter la source des interblocages. Les sous-sections suivantes présentent ces scénarios et la façon de contourner ces problèmes.

Implémentation dans les en-têtes

Dans des cas spécifiques, les implémentations de fonctions à l'intérieur des fichiers d'en-tête peuvent compliquer le diagnostic. Les fonctions inline et le code du modèle requièrent que les fonctions soient spécifiées dans un fichier d'en-tête. Le langage C++ spécifie la règle de définition unique, qui force toutes les implémentations de fonctions ayant le même nom à être sémantiquement équivalentes. Par conséquent, l'éditeur de liens C++ n'a pas besoin de faire de considérations spéciales lors de la fusion de fichiers objets qui ont des implémentations en double d'une fonction donnée.

En Visual C++ .NET et Visual C++ .NET 2003, l'éditeur de liens choisit simplement la plus importante de ces définitions sémantiquement équivalentes, afin de tenir compte des pré-déclarations et des scénarios lorsque différentes options d'optimisation sont utilisées pour des fichiers sources différents. Cela crée un problème pour les DLL natives/.NET mixtes.

Dans la mesure où le même en-tête peut à la fois être inclus par un fichier CPP avec le commutateur /clr activé et désactivé, ou qu'une instruction #include peut être enveloppée à l'intérieur d'un bloc #pragma unmanaged, il est possible d'avoir à la fois les versions MSIL et natives des fonctions qui fournissent les implémentations dans les en-têtes. Les implémentations MSIL et natives ont des sémantiques différentes en ce qui concerne l'initialisation lors du verrouillage du chargeur, ce qui viole en pratique la règle de définition unique. En conséquence, lorsque l'éditeur de liens choisit l'implémentation la plus importante, il peut choisir la version MSIL d'une fonction, même si elle a été compilée explicitement ailleurs en code natif à l'aide de la directive #pragma unmanaged. Pour faire en sorte qu'une version MSIL d'un modèle ou d'une fonction inline ne soit jamais appelée lors du verrouillage du chargeur, chaque définition de chaque fonction de ce genre appelée lors du verrouillage du chargeur doit être modifiée à l'aide de la directive #pragma unmanaged. Si le fichier d'en-tête provient d'une partie tierce, le procédé le plus simple pour ce faire consiste à exécuter un push et un pop de la directive #pragma unmanaged autour de la directive #include pour le fichier d'en-tête incriminé. (consultez managed, unmanaged pour obtenir un exemple). Toutefois, cette stratégie ne fonctionne pas pour les en-têtes qui contiennent un autre code qui doit appeler directement des API .NET.

À titre de commodité pour les utilisateurs confrontés au verrouillage du chargeur, l'éditeur de liens choisit l'implémentation native par rapport à l'implémentation managée en cas de présentation des deux implémentations. Cela évite les problèmes précités. Toutefois, il y a deux exceptions à cette règle dans cette version finale en raison de deux problèmes non résolus avec le compilateur :

  • L'appel de la fonction inline s'effectue à travers un pointeur de fonction statique globale. Ce scénario est particulièrement intéressant, parce que les fonctions virtuelles sont appelées à travers des pointeurs de fonction globale. Par exemple :
#include "definesmyObject.h"
#include "definesclassC.h"

typedef void (*function_pointer_t)();

function_pointer_t myObject_p = &myObject;

#pragma unmanaged
void DuringLoaderlock(C & c)
{
    // Either of these calls could resolve to a managed implementation, 
    // at link-time, even if a native implementation also exists.
    c.VirtualMember();
    myObject_p();
}
  • Avec la compilation ciblée Itanium, il existe un bogue dans l'implémentation de tous les pointeurs de fonction. Dans l'extrait de code ci-dessus, si myObject_p était défini localement à l'intérieur de during_loaderlock(), l'appel pourrait également être résolu en une implémentation managée.

Diagnostic en mode débogage

Tous les diagnostics des problèmes liés au verrouillage du chargeur doivent être effectués avec des versions Debug. Les versions Release ne peuvent pas produire de diagnostic et les optimisations réalisées en mode Release peuvent masquer une partie du langage MSIL dans les scénarios de verrouillage du chargeur.

Comment déboguer les problèmes de verrouillage du chargeur

Le diagnostic que le CLR génère lorsqu'une fonction MSIL est appelée provoque l'interruption de l'exécution du CLR. Cela entraîne ensuite l'interruption du débogueur en mode mixte de Visual C++ lors de l'exécution du programme débogué in-process. Toutefois, lors de l'attachement au processus, il n'est pas possible d'obtenir une pile des appels managée pour le programme débogué à l'aide du débogueur mixte.

Pour identifier la fonction MSIL spécifique qui a été appelée lors du verrouillage du chargeur, les développeurs doivent effectuer les étapes suivantes :

  1. Faire en sorte que les symboles de mscoree.dll et mscorwks.dll soient disponibles.

    Cette opération peut être effectuée de deux façons. Première façon : les fichiers PDB de mscoree.dll et mscorwks.dll peuvent être ajoutés au chemin de recherche de symboles. Pour ce faire, ouvrez la boîte de dialogue des options du chemin de recherche de symboles. (Dans le menu Outils, cliquez sur Options. Dans le volet gauche de la boîte de dialogue Options, ouvrez le nœud Débogage et cliquez sur Symboles.) Ajoutez à la liste de recherche le chemin d'accès aux fichiers PDB de mscoree.dll et mscorwks.dll. Ces PDB sont installés dans %VSINSTALLDIR%\SDK\v2.0\symbols. Cliquez sur OK.

    Seconde façon : les fichiers PDB de mscoree.dll et mscorwks.dll peuvent être téléchargés à partir de Microsoft Symbol Server. Pour configurer Symbol Server, ouvrez la boîte de dialogue des options du chemin de recherche de symboles. (Dans le menu Outils, cliquez sur Options. Dans le volet gauche de la boîte de dialogue Options, ouvrez le nœud Débogage et cliquez sur Symboles.) Ajoutez à la liste de recherche le chemin de recherche suivant : http://msdl.microsoft.com/download/symbols. Ajoutez un répertoire de cache de symboles à la zone de texte du cache du serveur de symboles. Cliquez sur OK.

  2. Définissez le mode du débogueur en mode natif uniquement.

    Pour ce faire, ouvrez la grille des propriétés du projet de démarrage dans la solution. Sous le sous-arbre Propriétés de configuration, sélectionnez le nœud Débogage. Affectez au champ Type de débogueur la valeur Natif uniquement.

  3. Démarrez le débogueur (F5).

  4. Lorsque le diagnostic /clr est généré, cliquez sur Réessayer, puis sur Arrêter.

  5. Ouvrez la fenêtre de la pile des appels. (Dans le menu Déboguer, cliquez sur Fenêtres, puis sur Pile des appels.) La fonction DllMain incriminée ou l'initialiseur statique est identifié avec une flèche verte. Si la fonction incriminée n'est pas identifiée, les étapes suivantes doivent être effectuées pour la rechercher.

  6. Ouvrez la fenêtre Exécution (dans le menu Déboguer, cliquez sur Fenêtres, puis sur Exécution).

  7. Tapez .load sos.dll dans la fenêtre Exécution pour charger le service de débogage SOS.

  8. Tapez !dumpstack dans la fenêtre Exécution pour obtenir une liste complète de la pile /clr interne.

  9. Recherchez la première instance (la plus proche du bas de la pile) de _CorDllMain (si DllMain est à l'origine du problème), ou de _VTableBootstrapThunkInitHelperStub ou GetTargetForVTableEntry (si l'initialiseur statique est à l'origine du problème). L'entrée de la pile juste en dessous de cet appel est l'appel de la fonction MSIL implémentée qui a tenté de s'exécuter lors du verrouillage du chargeur.

  10. Accédez au fichier source et au numéro de ligne identifiés à l'étape 9, puis corrigez le problème à l'aide des scénarios et des solutions décrits dans la section Scénarios.

Exemple

Description

L'exemple suivant explique comment éviter le verrouillage du chargeur en déplaçant le code de DllMain vers le constructeur d'un objet global.

Dans cet exemple, il existe un objet managé global dont le constructeur contient l'objet managé qui était initialement présent dans DllMain. La deuxième partie de cet exemple référence l'assembly, en créant une instance de l'objet managé pour appeler le constructeur de module qui exécute l'initialisation.

Code

// initializing_mixed_assemblies.cpp
// compile with: /clr /LD 
#pragma once
#include <stdio.h>
#include <windows.h>
struct __declspec(dllexport) A {
   A() {
      System::Console::WriteLine("Module ctor initializing based on global instance of class.\n");
   }

   void Test() {
      printf_s("Test called so linker does not throw away unused object.\n");
   }
};
 
#pragma unmanaged
// Global instance of object
A obj;
 
extern "C"
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) {
   // Remove all managed code from here and put it in constructor of A.
   return true;
}

Exemple

Code

// initializing_mixed_assemblies_2.cpp
// compile with: /clr initializing_mixed_assemblies.lib
#include <windows.h>
using namespace System;
#include <stdio.h>
#using "initializing_mixed_assemblies.dll"
struct __declspec(dllimport) A {
   void Test();
};

int main() {
   A obj;
   obj.Test();
}

Sortie

Module ctor initializing based on global instance of class.

Test called so linker does not throw away unused object.

Voir aussi

Concepts

Assemblys mixtes (natif et managé)