Initialisation d'assemblys mixtes

Les développeurs Windows doivent toujours se méfier du verrou du chargeur lors de l’exécution du code pendant DllMain. Toutefois, il existe des problèmes supplémentaires à prendre en compte lors du traitement des assemblys en mode mixte C++/CLI.

Le code dans DllMain ne doit pas accéder au Common Language Runtime (CLR) .NET. Cela signifie qu’il DllMain ne faut pas appeler des fonctions managées, directement ou indirectement ; aucun code managé ne doit être déclaré ou implémenté dans DllMain; et aucun chargement automatique de la bibliothèque ne doit avoir lieu dans DllMain.

Causes du verrouillage du chargeur

Avec l’introduction de la plateforme .NET, il existe deux mécanismes distincts pour le chargement d’un module d’exécution (EXE ou DLL) : un pour Windows, utilisé pour les modules non managés et un pour le CLR, qui charge des 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 contenant uniquement des constructions .NET est chargé dans un processus, le chargeur CLR peut effectuer toutes les tâches de chargement et d’initialisation nécessaires. Toutefois, pour charger des assemblys mixtes qui peuvent contenir du code et des données natifs, le chargeur Windows doit également être utilisé.

Le chargeur Windows garantit qu’aucun code ne peut accéder au code ou aux données de cette DLL avant son initialisation. Il garantit qu’aucun code ne peut charger de manière redondante la DLL pendant qu’elle est partiellement initialisée. Pour ce faire, le chargeur Windows utilise une section critique globale de processus (souvent appelée « verrou du chargeur ») qui empêche l’accès non sécurisé pendant l’initialisation du module. Le processus de chargement est donc vulnérable pour de nombreux scénarios d’interblocage classiques. Pour les assemblys mixtes, les deux scénarios suivants augmentent le risque d’interblocage :

  • Tout d’abord, si les utilisateurs tentent d’exécuter des fonctions compilées en langage MSIL (Microsoft Intermediate Language) lorsque le verrou du chargeur est conservé (à partir DllMain ou dans des initialiseurs statiques, par exemple), il peut provoquer un blocage. Considérez le cas dans lequel la fonction MSIL fait référence à un type dans un assembly qui n’est pas encore chargé. Le CLR tente de charger automatiquement cet assembly, ce qui peut nécessiter le blocage du chargeur Windows sur le verrouillage du chargeur. Un interblocage se produit, car le verrou du chargeur est déjà détenu par du code précédemment dans la séquence d’appels. Toutefois, l’exécution de MSIL sous le verrou du chargeur ne garantit pas qu’un blocage se produit. C’est ce qui rend ce scénario difficile à diagnostiquer et à corriger. Dans certaines circonstances, par exemple lorsque la DLL du type référencé ne contient aucune construction native et que toutes ses dépendances ne contiennent aucune construction native, le chargeur Windows n’est pas nécessaire pour charger l’assembly .NET du type référencé. En outre, l’assembly exigé ou ses dépendances natives/.NET mixtes ont peut-être déjà été chargés par un autre code. L’interblocage peut donc être difficile à prédire et peut varier selon la configuration de l’ordinateur cible.

  • Deuxièmement, lors du chargement de DLL dans les versions 1.0 et 1.1 du .NET Framework, le CLR suppose que le verrou du chargeur n’a pas été conservé et a effectué plusieurs actions non valides sous le verrou du chargeur. En supposant que le verrou du chargeur n’est pas conservé est une hypothèse valide pour les DLL .NET uniquement. Toutefois, étant donné que les DLL mixtes exécutent des routines d’initialisation natives, elles nécessitent le chargeur Windows natif et, par conséquent, le verrou du chargeur. Ainsi, même si le développeur n’a pas tenté d’exécuter de fonctions MSIL pendant l’initialisation de DLL, il y avait toujours une petite possibilité d’interblocage non déterministe dans .NET Framework versions 1.0 et 1.1.

Le non-déterminisme a été entièrement supprimé du processus de chargement de DLL mixtes Ces modifications ont été effectuées :

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

  • L’initialisation non managée et gérée est effectuée en deux étapes distinctes et distinctes. L’initialisation non managée a lieu en premier (via DllMain), et l’initialisation managée a lieu par la suite, par le biais d’un . Construction prise en charge par .cctor NET. Cette dernière est entièrement transparente pour l’utilisateur, sauf si /Zl elle est utilisée ou /NODEFAULTLIB utilisée. Pour plus d’informations, consultez (Ignorer les bibliothèques) et /Zl (Omettre le nom de la bibliothèque par défaut)./NODEFAULTLIB

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

Le reste de cet article décrit les scénarios restants pour lesquels MSIL peut s’exécuter sous le verrou du chargeur. Il montre comment résoudre le problème dans chacun de ces scénarios et les techniques de débogage.

Scénarios et solutions de contournement

Dans plusieurs cas de figure, le code utilisateur peut exécuter des instructions MSIL pendant le verrouillage du chargeur. Le développeur doit s’assurer que l’implémentation du code utilisateur ne tente pas d’exécuter des instructions MSIL dans chacune de ces circonstances. 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

La DllMain fonction est un point d’entrée défini par l’utilisateur pour une DLL. Sauf indication contraire de l’utilisateur, la fonction DllMain est appelée chaque fois qu’un processus ou 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 ici, 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, vous pouvez utiliser une stratégie de duplication où un .NET et une version native de la même fonction sont créés.

En guise d’alternative, si DllMain ce n’est pas nécessaire ou s’il n’a pas besoin d’être exécuté sous le verrou du chargeur, vous pouvez supprimer l’implémentation fournie par DllMain l’utilisateur, ce qui élimine le problème.

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

Pour plus d’informations sur ce scénario, consultez Obstacles au diagnostic.

Initialisation d'objets statiques

L’initialisation d’objets statiques peut entraîner un interblocage si un initialiseur dynamique est exigé. Les cas simples (par exemple, lorsque vous affectez une valeur connue au moment de la compilation à une variable statique) ne nécessitent pas d’initialisation dynamique. Il n’existe donc aucun risque d’interblocage. Toutefois, certaines variables statiques sont 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. Ces variables nécessitent tous que le code s’exécute pendant l’initialisation du module.

Le code suivant montre des exemples d’initialiseurs statiques qui exigent 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 avoir des définitions dans l’étendue globale, 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 dépend de la compilation /clr du module conteneur et de l’exécution de MSIL. Plus précisément, si la variable statique est compilée sans /clr (ou se trouve dans un #pragma unmanaged bloc) et que l’initialiseur dynamique requis pour l’initialiser entraîne l’exécution d’instructions MSIL, un blocage peut se produire. C’est parce que, pour les modules compilés sans /clr, l’initialisation des variables statiques est effectuée par DllMain. En revanche, les variables statiques compilées avec /clr sont initialisées par le .cctor, une fois l’étape d’initialisation non managée terminée et le verrou du chargeur a été libéré.

Il existe un certain nombre de solutions à l’interblocage provoqué par l’initialisation dynamique de variables statiques. Ils sont organisés ici à peu près dans l’ordre de temps 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 #pragma unmanaged directive.

  • 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 l’utilisateur dont dépendent les bibliothèques pour l’initialisation au démarrage. Par exemple, lorsque des opérateurs surchargés globalement en C++ tels que les opérateurs et delete les new opérateurs, les versions fournies par l’utilisateur sont utilisées partout, notamment dans l’initialisation et la destruction de la bibliothèque standard C++. Par conséquent, la bibliothèque standard C++ et les initialiseurs statiques fournis par l’utilisateur appellent toutes 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 tentent alors d’exécuter des instructions MSIL pendant que le verrouillage du chargeur est maintenu. Un utilisateur fourni malloc a les mêmes conséquences. Pour résoudre ce problème, l’une de ces surcharges ou définitions fournies par l’utilisateur doit être implémentée en tant que code natif à l’aide de la #pragma unmanaged directive.

Pour plus d’informations sur ce scénario, consultez Obstacles au diagnostic.

Paramètres régionaux personnalisés

Si l’utilisateur fournit des paramètres régionaux globaux personnalisés, ces paramètres régionaux sont utilisés pour initialiser tous les flux d’E/S futurs, y compris les flux qui sont initialisés statiquement. 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 options 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 . Il empêche l’exécution de leurs initialiseurs statiques sous le verrou du chargeur.

Les définitions de fonction de paramètres régionaux personnalisées peuvent être compilées en code natif à l’aide de la #pragma unmanaged directive.

Abstenez-vous de définir les paramètres régionaux personnalisés comme paramètres régionaux globaux tant que le verrouillage du chargeur n’a pas été désactivé. 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 exigent 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. L’éditeur de liens C++ n’a pas besoin de faire de considérations spéciales quand il fusionne des fichiers objets qui ont des implémentations en double d’une fonction donnée.

Dans les versions de Visual Studio avant Visual Studio 2005, l’éditeur de liens choisit simplement le plus grand de ces définitions sémantiquement équivalentes. Il est fait pour prendre en charge les déclarations de transfert et les scénarios où différentes options d’optimisation sont utilisées pour différents fichiers sources. Il crée un problème pour les DLL natives et .NET mixtes.

Étant donné que le même en-tête peut être inclus à la fois par les fichiers C++ avec /clr activé et désactivé, ou qu’un #include peut être encapsulé à l’intérieur d’un #pragma unmanaged bloc, il est possible d’avoir à la fois des versions MSIL et natives de fonctions qui fournissent des implémentations dans les en-têtes. Les implémentations MSIL et natives ont une sémantique différente pour l’initialisation sous le verrou du chargeur, ce qui enfreint efficacement la règle de définition unique. Par conséquent, lorsque l’éditeur de liens choisit la plus grande implémentation, il peut choisir la version MSIL d’une fonction, même si elle a été compilée explicitement dans du code natif ailleurs à l’aide de la #pragma unmanaged directive. Pour vous assurer qu’une version MSIL d’un modèle ou d’une fonction inline n’est jamais appelée sous le verrou du chargeur, chaque définition de chaque fonction appelée sous verrou de chargeur doit être modifiée avec la #pragma unmanaged directive. Si le fichier d’en-tête provient d’un tiers, le moyen le plus simple d’effectuer cette modification consiste à envoyer (push) et à afficher la #pragma unmanaged directive autour de la directive #include pour le fichier d’en-tête incriminé. (Consultez managed, unmanaged for an example.) Toutefois, cette stratégie ne fonctionne pas pour les en-têtes qui contiennent d’autres codes qui doivent 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. Cette valeur par défaut évite les problèmes ci-dessus. Toutefois, il existe deux exceptions à cette règle dans cette version en raison de deux problèmes non résolus avec le compilateur :

  • L’appel à une fonction inline est via un pointeur de fonction statique global. Ce scénario n’est pas table, car les fonctions virtuelles sont appelées par le biais de pointeurs de fonction globaux. 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();
}

Diagnostic en mode débogage

Toutes les opérations de diagnostic des problèmes liés au verrouillage du chargeur doivent être effectuées sur des builds Debug. Les builds de mise en production peuvent ne pas produire de diagnostics. Et les optimisations effectuées en mode Mise en production peuvent masquer certains des scénarios de verrouillage msIL sous chargeur.

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

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

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

  1. Vérifiez que les symboles de mscoree.dll et mscorwks.dll sont disponibles.

    Vous pouvez rendre les symboles disponibles de deux façons. Premièrement, les fichiers PDB de mscoree.dll et mscorwks.dll peuvent être ajoutés au chemin de recherche de symboles. Pour les ajouter, ouvrez la boîte de dialogue options du chemin de recherche des symboles. (À partir de Menu Outils , choisissez Options. Dans le volet gauche de la boîte de dialogue Options , ouvrez le nœud Débogage et choisissez Symboles.) Ajoutez le chemin d’accès aux fichiers PDB mscoree.dll et mscorwks.dll à la liste de recherche. Ces PDB sont installés dans %VSINSTALLDIR%\SDK\v2.0\symbols. Choisissez OK.

    Deuxièmement, les fichiers PDB de mscoree.dll et mscorwks.dll peuvent être téléchargés à partir du serveur de symboles Microsoft. Pour configurer le serveur de symboles, ouvrez la boîte de dialogue des options du chemin de recherche de symboles. (À partir de Menu Outils , choisissez Options. Dans le volet gauche de la boîte de dialogue Options , ouvrez le nœud Débogage et choisissez Symboles.) Ajoutez ce chemin de recherche à la liste de recherche : https://msdl.microsoft.com/download/symbols. Ajoutez un répertoire de cache de symboles à la zone de texte du cache du serveur de symboles. Choisissez OK.

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

    Ouvrez la grille Propriétés du projet de démarrage dans la solution. Sélectionnez Propriétés de configuration>Débogage. Définissez la propriété Type du débogueur sur Native-Only.

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

  4. Lorsque le /clr diagnostic est généré, choisissez Réessayer, puis Arrêt.

  5. Ouvrez la fenêtre Pile des appels. (Dans la barre de menus, choisissez Déboguer la>pile des appels Windows>.) L’initialiseur incriminé DllMain ou statique est identifié par une flèche verte. Si la fonction incriminées n’est pas identifiée, les étapes suivantes doivent être effectuées pour la trouver.

  6. Ouvrez la fenêtre Exécution (dans la barre de menus, choisissez Déboguer>Windows>Exécution.)

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

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

  9. Recherchez la première instance (la plus proche du bas de la pile) de _CorDllMain (si DllMain cela provoque le problème) ou _VTableBootstrapThunkInitHelperStub ou GetTargetForVTableEntry (si un initialiseur statique provoque le 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 pendant le verrouillage du chargeur.

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

Exemple

Description

L’exemple suivant montre comment éviter le verrouillage du chargeur en déplaçant le code vers DllMain 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 dans DllMain. La deuxième partie de cet exemple fait référence à l’assembly, créant une instance de l’objet managé pour appeler le constructeur de module qui effectue 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 doesn't 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;
}

Cet exemple illustre les problèmes d’initialisation d’assemblys mixtes :

// 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();
}

Ce code génère la sortie suivante :

Module ctor initializing based on global instance of class.

Test called so linker doesn't throw away unused object.

Voir aussi

Assemblys mixtes (natif et managé)