Delen via


Initialisatie van gemengde assemblages

Windows-ontwikkelaars moeten altijd op hun hoede zijn voor loader lock tijdens het uitvoeren van code DllMain. Er zijn echter enkele extra problemen waarmee u rekening moet houden bij het verwerken van C++/CLI-assembly's in gemengde modus.

Code in DllMain mag geen toegang krijgen tot .NET Common Language Runtime (CLR). Dat betekent dat DllMain geen aanroepen naar beheerde functies mogen worden uitgevoerd, direct of indirect; er geen beheerde code moet worden gedeclareerd of geïmplementeerd in DllMain; en dat er geen afvalverzameling of automatisch bibliotheken laden mag plaatsvinden binnen DllMain.

Oorzaken van Loader Lock

Met de introductie van het .NET-platform zijn er twee verschillende mechanismen voor het laden van een uitvoeringsmodule (EXE of DLL): een voor Windows, die wordt gebruikt voor niet-beheerde modules en een voor de CLR, waarmee .NET-assembly's worden geladen. Het gemengde DLL-laadprobleem centreert zich rond het Microsoft Windows-besturingssysteemlaadprogramma.

Wanneer een assembly met alleen .NET-constructies in een proces wordt geladen, kan het CLR-laadprogramma alle benodigde laad- en initialisatietaken zelf uitvoeren. Als u echter gemengde assembly's wilt laden die systeemeigen code en gegevens kunnen bevatten, moet het Windows-laadprogramma ook worden gebruikt.

Het Windows-laadprogramma garandeert dat er geen code toegang heeft tot code of gegevens in die DLL voordat deze is geïnitialiseerd. Het zorgt ervoor dat de DLL niet onnodig kan worden geladen terwijl deze gedeeltelijk wordt geïnitialiseerd. Hiervoor maakt het Windows-laadprogramma gebruik van een proceskritieke sectie (ook wel 'loadervergrendeling' genoemd) die onveilige toegang voorkomt tijdens de initialisatie van de module. Hierdoor is het laadproces kwetsbaar voor veel klassieke impassescenario's. Voor gemengde assembly's nemen de volgende twee scenario's het risico op impasses toe:

  • Als gebruikers eerst proberen functies uit te voeren die zijn gecompileerd naar de Tussenliggende Taal van Microsoft (MSIL) wanneer de loadervergrendeling wordt vastgehouden (van DllMain of in statische initializers, bijvoorbeeld), kan dit een impasse veroorzaken. Houd rekening met het geval waarin de functie MSIL verwijst naar een type in een assembly die nog niet is geladen. De CLR zal proberen die assembly automatisch te laden, wat ertoe kan leiden dat de Windows-loader moet wachten vanwege de loader lock. Er treedt een impasse op, omdat de vergrendeling van de lader al wordt vastgehouden door code eerder in de oproepvolgorde. Het uitvoeren van MSIL onder de ladervergrendeling garandeert echter niet dat er een impasse optreedt. Dat maakt dit scenario moeilijk te diagnosticeren en op te lossen. In sommige gevallen, zoals wanneer het DLL-bestand van het type waarnaar wordt verwezen geen systeemeigen constructies bevat en alle bijbehorende afhankelijkheden geen systeemeigen constructies bevatten, is het Windows-laadprogramma niet vereist om de .NET-assembly van het type waarnaar wordt verwezen te laden. Daarnaast kan het zijn dat de vereiste assembly of de gemengde native/.NET-afhankelijkheden al door andere code zijn geladen. Daarom kan de impasse moeilijk te voorspellen zijn en kan variëren, afhankelijk van de configuratie van de doelcomputer.

  • Ten tweede, bij het laden van DLL's in versie 1.0 en 1.1 van het .NET Framework, ging de CLR ervan uit dat de loadervergrendeling niet is vastgehouden en verschillende acties heeft uitgevoerd die ongeldig zijn onder het laadlaadslot. Ervan uitgaande dat de loadervergrendeling niet wordt vastgehouden, is een geldige aanname voor uitsluitend .NET-DLL's. Maar omdat gemengde DLL's systeemeigen initialisatieroutines uitvoeren, vereisen ze het systeemeigen Windows-laadprogramma en dus de loadervergrendeling. Zelfs als de ontwikkelaar tijdens de DLL-initialisatie geen MSIL-functies probeerde uit te voeren, was er nog steeds een kleine kans op een niet-deterministische impasse in .NET Framework-versies 1.0 en 1.1.

Alle niet-determinisme is verwijderd uit het gemengde DLL-laadproces. Dit is bereikt met deze wijzigingen:

  • De CLR maakt geen valse veronderstellingen meer bij het laden van gemengde DLL's.

  • Niet-beheerde en beheerde initialisatie wordt uitgevoerd in twee afzonderlijke en afzonderlijke fasen. Onbeheerde initialisatie vindt eerst plaats (via DllMain), en beheerde initialisatie vindt daarna plaats via een . Door NET ondersteunde .cctor constructie. De laatste is volledig transparant voor de gebruiker, tenzij /Zl of /NODEFAULTLIB worden gebruikt. Zie (Bibliotheken negeren) en (Standaardbibliotheeknaam weglaten) voor meer informatie/NODEFAULTLIB./Zl

De loadervergrendeling kan nog steeds optreden, maar nu gebeurt het reproduceerbaar en wordt gedetecteerd. Als DllMain MSIL-instructies bevat, genereert de compiler compilerwaarschuwing (niveau 1) C4747. Bovendien probeert de CRT of de CLR pogingen om MSIL uit te voeren onder laadlaadvergrendeling te detecteren en te rapporteren. CRT-detectie resulteert in runtimediagnose C Run-Time Fout R6033.

In de rest van dit artikel worden de overige scenario's beschreven waarin MSIL kan worden uitgevoerd onder de loader lock. Het laat zien hoe u het probleem onder elk van deze scenario's en foutopsporingstechnieken kunt oplossen.

Scenario's en tijdelijke oplossingen

Er zijn verschillende situaties waarin gebruikerscode MSIL kan uitvoeren onder laadlaadvergrendeling. De ontwikkelaar moet ervoor zorgen dat de implementatie van de gebruikerscode niet probeert MSIL-instructies uit te voeren onder elk van deze omstandigheden. In de volgende subsecties worden alle mogelijkheden beschreven met een discussie over het oplossen van problemen in de meest voorkomende gevallen.

DllMain

De DllMain functie is een door de gebruiker gedefinieerd toegangspunt voor een DLL. Tenzij de gebruiker anders opgeeft, DllMain wordt elke keer aangeroepen wanneer een proces of thread wordt gekoppeld aan of loskoppelt van het dll-bestand. Omdat deze aanroep kan optreden terwijl de laadladervergrendeling wordt vastgehouden, moet er geen door de gebruiker geleverde DllMain functie worden gecompileerd naar MSIL. Verder kan er geen functie in de aanroepstructuur met wortel bij DllMain worden gecompileerd naar MSIL. Om problemen hier op te lossen, moet het codeblok waarmee wordt gedefinieerd DllMain , worden gewijzigd met #pragma unmanaged. Hetzelfde moet worden gedaan voor elke functie die DllMain aanroept.

In gevallen waarin deze functies een functie moeten aanroepen waarvoor een MSIL-implementatie is vereist voor andere aanroepende contexten, kunt u een duplicatiestrategie gebruiken waarbij zowel een .NET- als een systeemeigen versie van dezelfde functie worden gemaakt.

Als alternatief kunt u, als DllMain niet is vereist of als het niet onder de loadervergrendeling hoeft te worden uitgevoerd, de door de gebruiker geleverde DllMain-implementatie verwijderen, waardoor het probleem wordt weggenomen.

Als DllMain probeert MSIL direct uit te voeren, resulteert Compilerwaarschuwing (niveau 1) C4747. De compiler kan echter geen gevallen detecteren waarbij DllMain een functie wordt aangeroepen in een andere module die op zijn beurt probeert MSIL uit te voeren.

Zie Belemmeringen voor diagnose voor meer informatie over dit scenario.

Statische objecten initialiseren

Het initialiseren van statische objecten kan leiden tot een impasse als een dynamische initialisatiefunctie vereist is. Eenvoudige gevallen (zoals wanneer u een waarde toewijst die bekend is tijdens het compileren aan een statische variabele) vereisen geen dynamische initialisatie, dus er is geen risico op impasses. Sommige statische variabelen worden echter geïnitialiseerd door functie-aanroepen, constructor-aanroepen of expressies die niet kunnen worden geëvalueerd tijdens het compileren. Deze variabelen vereisen allemaal code die moet worden uitgevoerd tijdens de initialisatie van de module.

In de onderstaande code ziet u voorbeelden van statische initialisatiefuncties waarvoor dynamische initialisatie is vereist: een functie-aanroep, objectconstructie en een aanwijzer-initialisatie. (Deze voorbeelden zijn niet statisch, maar worden ervan uitgegaan dat ze definities hebben in het globale bereik, wat hetzelfde effect heeft.)

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

Dit risico op een deadlock is afhankelijk van of de module die het bevat is gecompileerd met /clr en of MSIL zal worden uitgevoerd. Als de statische variabele is gecompileerd zonder /clr (of zich in een #pragma unmanaged blok bevindt) en de dynamische initializer die vereist is om deze te initialiseren resulteert in de uitvoering van MSIL-instructies, kan er een impasse optreden. Dit komt doordat voor modules die zonder /clrzijn gecompileerd, de initialisatie van statische variabelen wordt uitgevoerd door DllMain. Statische variabelen die zijn gecompileerd met /clr worden daarentegen door de .cctor geïnitialiseerd, nadat de niet-beheerde initialisatiefase is voltooid en de loadervergrendeling is vrijgegeven.

Er zijn een aantal oplossingen voor impasses die worden veroorzaakt door de dynamische initialisatie van statische variabelen. Ze zijn hier ongeveer gerangschikt in volgorde van tijd die nodig is om het probleem op te lossen:

  • Het bronbestand met de statische variabele kan worden gecompileerd met /clr.

  • Alle functies die door de statische variabele worden aangeroepen, kunnen worden gecompileerd naar systeemeigen code met behulp van de #pragma unmanaged instructie.

  • Kloon handmatig de code waarvan de statische variabele afhankelijk is, en geef zowel een .NET- als een systeemeigen versie met verschillende namen op. Ontwikkelaars kunnen vervolgens de systeemeigen versie aanroepen vanuit systeemeigen statische initializers en de .NET-versie elders aanroepen.

User-Supplied functies die van invloed zijn op het opstarten

Er zijn verschillende door de gebruiker geleverde functies waarvan bibliotheken afhankelijk zijn voor initialisatie tijdens het opstarten. Wanneer wereldwijd operators in C++ zoals de new en delete operators worden overbelast, worden de door de gebruiker geleverde versies overal gebruikt, inclusief bij de initialisatie en vernietiging van de C++-standaardbibliotheek. Als gevolg hiervan roepen C++ Standard Library en door de gebruiker geleverde statische initializers alle door de gebruiker geleverde versies van deze operators aan.

Als de door de gebruiker geleverde versies worden gecompileerd naar MSIL, proberen deze initialisatieprogramma's MSIL-instructies uit te voeren terwijl de loadervergrendeling wordt vastgehouden. Een door de gebruiker opgegeven malloc heeft dezelfde gevolgen. Om dit probleem op te lossen, moet een van deze overbelastingen of door de gebruiker geleverde definities worden geïmplementeerd als systeemeigen code met behulp van de #pragma unmanaged richtlijn.

Zie Belemmeringen voor diagnose voor meer informatie over dit scenario.

Aangepaste landinstellingen

Als de gebruiker een aangepaste globale landinstelling biedt, wordt deze landinstelling gebruikt om alle toekomstige I/O-streams te initialiseren, inclusief streams die statisch zijn geïnitialiseerd. Als dit globale landinstellingsobject naar MSIL is gecompileerd, kunnen lidfuncties van het landinstellingsobject die naar MSIL zijn gecompileerd, worden aangeroepen terwijl de laadvergrendeling vastgehouden wordt.

Er zijn drie opties voor het oplossen van dit probleem:

De bronbestanden met alle globale I/O-stroomdefinities kunnen worden gecompileerd met behulp van de /clr optie. Hiermee voorkomt u dat hun statische initialisatieprogramma's worden uitgevoerd onder de loadervergrendeling.

De aangepaste landinstellingsfunctiedefinities kunnen worden gecompileerd naar systeemeigen code met behulp van de #pragma unmanaged instructie.

Stel de aangepaste landinstelling niet in als de globale landinstelling totdat de loadervergrendeling is vrijgegeven. Configureer vervolgens expliciet I/O-streams die zijn gemaakt tijdens de initialisatie met de aangepaste landinstelling.

Belemmeringen voor diagnose

In sommige gevallen is het moeilijk om de bron van impasses te detecteren. In de volgende subsecties worden deze scenario's en manieren besproken om deze problemen te omzeilen.

Implementatie in headers

In bepaalde gevallen kunnen functie-implementaties in headerbestanden de diagnose bemoeilijken. Voor inlinefuncties en sjablooncode moeten beide functies worden opgegeven in een headerbestand. De C++-taal geeft de regel voor één definitie op, waardoor alle implementaties van functies met dezelfde naam semantisch gelijkwaardig moeten zijn. Daarom hoeft de C++-linker geen speciale overwegingen te maken bij het samenvoegen van objectbestanden met dubbele implementaties van een bepaalde functie.

In Visual Studio-versies vóór Visual Studio 2005 kiest de linker gewoon de grootste van deze semantisch equivalente definities. Het wordt gedaan om vooruitdeclaraties te accommoderen en voor scenario's waarin verschillende optimalisatieopties worden gebruikt voor verschillende bronbestanden. Er ontstaat een probleem voor gemengde systeemeigen en .NET-DLL's.

Omdat dezelfde header zowel door C++-bestanden met /clr ingeschakeld als uitgeschakeld kan worden opgenomen, of een #include kan worden omgeven door een #pragma unmanaged blok, is het mogelijk om zowel MSIL- als systeemeigen versies van functies te hebben die implementaties in headers bieden. MSIL en systeemeigen implementaties hebben verschillende semantiek voor initialisatie onder de loadervergrendeling, die de ene definitieregel effectief schendt. Wanneer de linker de grootste implementatie kiest, kan deze dus de MSIL-versie van een functie kiezen, zelfs als deze expliciet is gecompileerd naar systeemeigen code elders met behulp van de #pragma unmanaged richtlijn. Om ervoor te zorgen dat een MSIL-versie van een sjabloon of inlinefunctie nooit wordt aangeroepen onder laadlaadvergrendeling, moet elke definitie van elke dergelijke functie die onder laadlaadvergrendeling wordt genoemd, worden gewijzigd met de #pragma unmanaged richtlijn. Als het headerbestand afkomstig is van een derde partij, is de eenvoudigste manier om deze wijziging aan te brengen door de #pragma unmanaged instructie rond de #include-instructie voor het betreffende headerbestand te pushen en poppen. (Zie beheerd, onbeheerd voor een voorbeeld.) Deze strategie werkt echter niet voor headers die andere code bevatten die rechtstreeks .NET-API's moeten aanroepen.

Ter ondersteuning van gebruikers die te maken hebben met loadervergrendeling, zal de linker de voorkeur geven aan de systeemeigen implementatie boven de beheerde, wanneer beide beschikbaar zijn. Deze standaardinstelling voorkomt de bovenstaande problemen. Er zijn echter twee uitzonderingen op deze regel in deze release vanwege twee onopgeloste problemen met de compiler:

  • De aanroep van een inlinefunctie verloopt via een globale statische functieaanwijzer. Dit scenario is niet mogelijk omdat virtuele functies worden aangeroepen via globale functieaanwijzers. Bijvoorbeeld
#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();
}

Diagnose uitvoeren in de foutopsporingsmodus

Alle diagnoses van loadervergrendelingsproblemen moeten worden uitgevoerd met debug builds. Release-builds produceren mogelijk geen diagnostische gegevens. En de optimalisaties die in de Release-modus zijn gemaakt, kunnen enkele van de MSIL-scenario's onder loader lock maskeren.

Problemen met het vergrendelen van laders opsporen

De diagnose die de CLR genereert wanneer een MSIL-functie wordt aangeroepen, zorgt ervoor dat de CLR de uitvoering onderbreekt. Hierdoor wordt het foutopsporingsprogramma in de gemengde modus van Visual C++ ook onderbroken bij het uitvoeren van de foutopsporing in het proces. Bij het koppelen aan het proces is het echter niet mogelijk om een beheerde callstack te verkrijgen voor de foutopsporing met behulp van het gemengde foutopsporingsprogramma.

Om de specifieke MSIL-functie te identificeren die is aangeroepen onder de loadervergrendeling, moeten ontwikkelaars de volgende stappen uitvoeren:

  1. Zorg ervoor dat symbolen voor mscoree.dll en mscorwks.dll beschikbaar zijn.

    U kunt de symbolen op twee manieren beschikbaar maken. Eerst kunnen de PDBs voor mscoree.dll en mscorwks.dll worden toegevoegd aan het zoekpad voor symbolen. Als u ze wilt toevoegen, opent u het dialoogvenster met zoekpadopties voor symbolen. (Kies Opties in het menu Extra. Open in het linkerdeelvenster van het dialoogvenster Opties het knooppunt Foutopsporing en kies Symbolen.) Voeg het pad naar de mscoree.dll- en mscorwks.dll-PDB-bestanden toe aan de zoeklijst. Deze PDBs worden geïnstalleerd op de %VSINSTALLDIR%\SDK\v2.0\symbols. Kies OK.

    Ten tweede kunnen de PDBs voor mscoree.dll en mscorwks.dll worden gedownload van de Microsoft Symbol Server. Als u Symboolserver wilt configureren, opent u het dialoogvenster met opties voor het zoekpad voor symbolen. (Kies Opties in het menu Extra. Open in het linkerdeelvenster van het dialoogvenster Opties het knooppunt Foutopsporing en kies Symbolen.) Voeg dit zoekpad toe aan de zoeklijst: https://msdl.microsoft.com/download/symbols. Voeg een cachemap voor symbolen toe aan het tekstvak voor de cache van de symboolserver. Kies OK.

  2. Stel de debugmodus in op uitsluitend systeemeigen modus.

    Open de Eigenschappenraster voor het opstartproject in de oplossing. Selecteer Configuratie-eigenschappen>Foutopsporing. Stel de eigenschap Foutopsporingsprogrammatype in op Alleen systeemeigen.

  3. Start het foutopsporingsprogramma (F5).

  4. Wanneer de /clr diagnose wordt gegenereerd, kies Opnieuw proberen en kies vervolgens Onderbreken.

  5. Open het aanroepstackvenster. Kies in de menubalk Debug>Windows>Call Stack. De problematische DllMain of statische initialisatiefunctie wordt geïdentificeerd met een groene pijl. Als de problematische functie niet wordt geïdentificeerd, moeten de volgende stappen worden uitgevoerd om deze te vinden.

  6. Open het venster Direct (kies op de menubalk Debug>Windows>Direct.)

  7. Voer .load sos.dll in het venster Direct in om de SOS-foutopsporingsservice te laden.

  8. Voer !dumpstack in het Direct venster in om een volledige lijst van de interne /clr stack te verkrijgen.

  9. Zoek naar het eerste voorbeeld (zo dicht mogelijk bij de onderkant van de stack) van _CorDllMain (als DllMain het probleem veroorzaakt) of _VTableBootstrapThunkInitHelperStub of GetTargetForVTableEntry (als een statische initialisatie het probleem veroorzaakt). De stackvermelding net onder deze aanroep is de aanroep van de geïmplementeerde MSIL-functie die probeerde uit te voeren onder laadvergrendeling.

  10. Ga naar het bronbestand en het regelnummer dat in de vorige stap is geïdentificeerd en corrigeer het probleem met behulp van de scenario's en oplossingen die worden beschreven in de sectie Scenario's.

Voorbeeld

Beschrijving

In het volgende voorbeeld ziet u hoe u een loader lock kunt voorkomen door code te verplaatsen naar de constructor van een globaal object.

In dit voorbeeld is er een globaal beheerd object waarvan de constructor het beheerde object bevat dat oorspronkelijk in DllMain. Het tweede deel van dit voorbeeld verwijst naar de assembly, waarmee een exemplaar van het beheerde object wordt gemaakt om de moduleconstructor aan te roepen die de initialisatie doet.

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;
}

In dit voorbeeld ziet u problemen bij het initialiseren van gemengde assembly's:

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

Deze code produceert de volgende uitvoer:

Module ctor initializing based on global instance of class.

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

Zie ook

Gemengde (systeemeigen en beheerde) assembly's