Sdílet prostřednictvím


Inicializace smíšených sestavení

V aplikaci Visual C++ .NET a Visual C++ 2003 se mohou DLL knihovny kompilovány s možností kompilátoru /clr nedeterministicky vzájemně zablokovat při načítání; tomuto problému se říkalo smíšené načítání DLL knihovny nebo problém uzamknutí zavaděče. Téměř bez determinism byl odebrán ze smíšených procesu načítání knihovny DLL. Avšak existuje několik zbývajících scénářů pro které se může provést uzamknutí zavaděče (deterministicky). Další informace o tomto problému naleznete v části "Smíšené DLL načítání problém" v knihovny MSDN.

Kód v DllMain musí přistupovat CLR. To znamená, že DllMain by neměla provést žádná volání spravovaných funkcí přímo nebo nepřímo; žádný spravovaný kód by neměl být deklarován nebo implementován v DllMain; a žádné uvolňování paměti nebo automatické načítání knihovna by nemělo být v rámci DllMain.

Poznámka

Visual C++ 2003 nabízí _vcclrit.h k usnadnění inicializace DLL knihovny při minimalizaci příležitosti vzájemného zablokování. Pomocí _vcclrit.h již není nutné a způsobuje varování o odmítání příznaku vyráběné během kompilace. Doporučená strategie je odebrání závislostí na tento soubor, pomocí kroků v How To: Remove Dependency on _vcclrit.h. Méně ideální řešení zahrnují potlačení upozornění, definováním _CRT_VCCLRIT_NO_DEPRECATE dříve než vložíte _vcclrit.h nebo pouze ignorováním odstranění upozornění.

Příčiny uzamknutí zavaděče

Se zavedením platformy .NET existují dva odlišné mechanismy pro načítání spustitelného modulu (EXE nebo knihovna DLL): jeden pro Windows, ktery se používá pro nespravované moduly, a jeden pro .NET modul CLR, který načte sestavení .NET. Smíšený problém načtení knihovny DLL se pohybuje okolo Zavaděče operačního systému Microsoft WIndows.

Pokud sestavení obsahuje pouze konstrukce .NET načtené do procesu, může zavaděč modulu CLR provést všechny nezbytné načtení a inicializace. Avšak u smíšených sestavení musí být také použito zavaděče systému Windows, protože může obsahovat nativní kód a data.

Zavaděč systému Windows zaručuje, že žádný kód nemůžete přistoupit ke kódu nebo k datům v této DLL knihovně před její inicializací a žádný kód nemůže redundantně načíst DLL knihovnu při částečné inicializaci. To provedete tak, že zavaděč systému Windows používá procesně globální kritickou sekci (často nazývanou "uzamknutý zavaděč"), která zabraňuje nebezpečnému přístupu během inicializace modulu. V důsledku toho je proces načítání ohrožen v mnoha scénářích klasického vzájemného zablokování. Pro smíšené sestavení v následujících dvou scénářích se zvýšují rizika vzájemného zablokování:

  • První, pokud se uživatelé pokusí spustit funkce, zkompilované tak, aby se vykonal jazyk MSIL při uzamknutí zavaděče (z DllMain nebo ve statických inicializátorech, například), může dojít k vzájemnému zablokování. Zvážte případ, v němž funkce jazyka MSIL odkazuje na typ v sestavení, který není načten. Modul CLR se pokusí automaticky načíst toto sestavení, které může vyžadovat zavaděč systému Windows k zablokování uzamknutí zavaděče. Vzhledem k tomu, že uzamčení zavaděče je již v držení kódem dříve zavolaného v sekvence, nastane vzájemné zablokování. Avšak spuštění jazyka MSIL pod uzamčeným zavaděčem nezaručuje, že dojde k vzájemnému zablokování, dělá tento scénář těžším k diagnostice a opravení. Za určitých okolností, například pokud DLL knihovna odkazovaného typu neobsahuje žádné nativní konstrukce a všechny jeho závislosti neobsahují žádné nativní konstrukce, není vyžadováno zavaděče systému Windows k načtení sestavení .NET odkazovaného typu. Kromě toho bylo požadované sestavení nebo jeho smíšené nativní/.NET závislosti již načteno jiným kódem. Proto může být složité odhadnout vzájemné zablokování a může se lišit podle konfigurace cílového počítače.

  • Druhý, když se načítají DLL knihovny ve verzi 1.0 a rozhraní .NET Framework ve verzi 1.1, předpokládá modul CLR, že uzamčení zavaděče není vykonáno a provede se několik akcí, které jsou neplatné pod uzamčeným zavaděčem. Za předpokladu, že není vykonáno uzamčení zavaděče je platným předpokladem pro čisté DLL knihovny .NET, ale protože smíšené knihovny DLL provádějí inicializaci nativních rutin, vyžadují nativní zavaděč systému Windows a proto uzamknou zavaděč. V důsledku toho i v případě, že vývojář se nepokouší provést žádné funkce jazyka MSIL během inicializace DLL knihovny, existuje stále malá možnost nedeterministicého vzájemného zablokování s verzí 1.0 a rozhraním .NET Framework ve verzi 1.1.

Všechny jiné determinism byl odebrán z smíšené procesu načítání knihovny DLL. To byl provedeno s těmito změnami:

  • Modul CLR již neprování chybné předpoklady při načítání smíšených DLL knihoven.

  • Spravované a nespravované inicializace probíhají ve dvou oddělených a rozdílných etapách. Nespravovaná inicializace probíhá jako první (prostřednictvím DllMain) a spravovaná inicializace probíhá později, prostřednictvím .NET podporované konstrukce, nazývané .cctor. Poslední je plně transparentní pro uživatele, pokud jsou používány /Zl nebo /NODEFAULTLIB. Další informace naleznete v tématu /NODEFAULTLIB (Ignore Libraries) a /Zl (Omit Default Library Name).

Stále může dojít k uzamčení zavaděče, ale nyný k němu dojde reprodukovaně a je rozpoznán. Pokud DllMain obsahuje instrukce jazyka MSIL, vygeneruje kompilátor upozornění Compiler Warning (level 1) C4747. Kromě toho se pokusí CRT nebo CLR zjistit a ohlásit pokusy o provedení jazyka MSIL při uzamčeném zavaděči. CRT detekuje výsledky za běhu diagnostiky běhové chyby R6033 jazyka C.

Zbývající část tohoto dokument popisuje zbývající scénáře, pro které lze provést jazyk MSIL pod uzamčeným zavaděčeem, řešení problému pro každý z těchto scénářů a techniky ladění.

Scénáře a zástupná řešení

Existuje několik různých situací, ve kterých může uživatel provést kód jazyka MSIL pod uzamčeným zavaděčem. Vývojář musí zajistit, že se implementace kódu uživatele nebude pokoušet provádět instrukce jazyka MSIL pro každou z těchto okolností. Následující pododdíly popisují všechny možnosti s diskusi jak vyřšit problémy v nejběžnějších případech.

  • DllMain

  • statické inicializátory

  • Uživatelem dodané funkce ovlivňující spuštění

  • vlastní národní prostředí

DllMain

Funkce DllMain je uživatelský definovaným vstupním bodem pro knihovnu DLL. Pokud uživatel neurčí jinak, je pokaždé vyvolán proces DllMain nebo přidělení vlákna nebo odebrání z obsahující DLL knihovny. Vzhledem k tomu, že k tomuto vyvolání může dojít při uchování uzamčení zavaděče, uživatelem nepodporovaná funkce DllMain by měla být zkompilována pro jazyk MSIL. Kromě toho žádná funkce v kořenovém stromě volání v DllMain nemůže být zkompilována pro jazyk MSIL. Vyřešení těchto problémů, by měl být blok kódu, který definuje DllMain změněn s #pragma unmanaged. Totéž by mělo být provedeno pro každou funkce daného volání DllMain.

V případech, kde tyto funkce musí volat funkci, která vyžaduje implementaci jazyka MSIL pro další kontexty volání, může být použita duplikační strategie kde jsou vytvořeny .NET i nativní verze stejné funkce.

Případně pokud není vyžadováno DllMain nebo pokud to není vyžadováno ke spuštění při uzamčeném zavaděči, uživatelem poskytnutá implementace DllMain může být odebrána, což odstraní problém.

Pokud se DllMain pokusí provést přímo jazyk MSIL, bude provedeno Compiler Warning (level 1) C4747. Avšak kompilátor nemůže rozpoznat případy, kde DllMain volá funkci v jiném modulu, který se opět pokusí provést jazyk MSIL.

Podívejte se prosím na "Překážky pro diagnostiku" pro více informací o tomto scénáři.

Inicializace statických objektů

Inicializace statických objektů může mít za následek vzájemné zablokování, pokud se vyžadován dynamický inicializátor. Pro jednoduché případy, jako je například statická proměnná, je jednoduše přiřazena k hodnotě známé v době kompilace, není vyžadováno žádné dynamické inicializace, proto není riziko vzájemného zablokování. Avšak statické proměnné, inicializované voláním funkce, vyvoláním konstruktoru nebo výrazy, které nelze vyhodnotit během doby kompilace celého vyžadovaného kódu k provedení během inicializace modulu.

Následující kód uvádí příklady statických inicializátorů, které vyžadují dynamickou inicializaci: volání funkce, vytvoření objektu a inicializace ukazatele. (Tyto příklady nejsou statické, ale jsou považovány jako definované v globálním rozsahu, což má stejný účinek.)

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

Toto riziko vzájemného zablokování závisí na tom, zda je obsahující modul kompilována s /clr a zda bude proveden jazyk MSIL. Konkrétně v případě, že statická proměnná je kompilována bez /clr (nebo se nachází v bloku #pragma unmanaged) a je vyžadováno dynamické inicializace k inicializaci jeho výsledku v provádění instrukcí jazyka MSIL, může dojít k vzájemnému zablokování. Důvodem je, že inicializace statických proměnných pro moduly zkompilované bez /clr je prováděna v DllMain. Naopak statické proměnné, kompilované s /clr, jsou iniciovány .cctor po dokončení nespravované inicializační fázi a vykonání uzamčení zavaděče.

Existuje několik řešení, která řeší vzájemné zablokování, způsobené dynamickou inicializací statických proměnných (uspořádány přibližně v pořadí doby nutné k vyřešení problému):

  • zdrojový soubor, obsahující statickou proměnnou, může být zkompilován s /clr.

  • Všechny funkce, které jsou volány statickou proměnnou, mohou být zkompilovány k použití nativním kódem směrnic #pragma unmanaged.

  • Ručně zkopírujte kód, který závisí na statické proměnné, poskytující .NET a nativní verzi s různými názvy. Vývojáři mohou poté zavolat nativní verzi z nativního statického inicializátoru a jinde volat verze rozhraní .NET.

Uživatelem dodané funkce ovlivňující spuštění

Existuje několik funkcí zadaných uživatelem na kterých závisí knihovny pro inicializaci během spuštění. Například při globálním přetížení operátorů v jazyce C++, jako například operátory new a delete, verze zadané uživatelem slouží všude, včetně STL inicializace a destrukce. V důsledku toho, vyvolají STL a uživatelem zadané statické inicializátory jakékoliv uživatelské verze těchto operátorů.

Pokud jsou uživatelské verze zkompilovány pro jazyka MSIL, pak se budou tyto inicializátory pokoušet provádět instrukce jazyka MSIL v době, kdy je uzamčen zavaděč. Uživatelem zadaný malloc má stejné důsledky. Chcete-li tento problém vyřešit, některé tyto přetížení nebo uživatelem zadané definice musí být implementovány v nativním kódu, použitím směrnice #pragma unmanaged.

Podívejte se prosím na "Překážky pro diagnostiku" pro více informací o tomto scénáři.

Vlastní národní prostředí

Pokud uživatel poskytne vlastní globální národní prostředí, bude toto národní prostředí použito pro inicializaci všech budoucích Vstupně/Výstupních proudů, které jsou staticky inicializovány. Pokud je tento objekt globálního národního prostředí zkompilován pro jazyk MSIL, potom členské funkce objektu národního prostředí, zkompilované pro jazyk MSIL, mohou být vyvolány během uzamčení zavaděče.

Existují tři možnosti pro řešení tohoto problému:

Zdrojové soubory, obsahující všechny definice globálních Vstupně/Výstupních proudů, mohou být zkompilovány, použitím volby /clr. Tím zabráníte jejich statickým inicializátorům provádět se v rámci uzamčeného zavaděče.

Definice funkcí vlastního národního prostředí mohou být zkompilovány v nativním kódu, použitím směrnice #pragma unmanaged.

Zdržte se nastavení vlastního národní prostředí jako globálního národního prostředí až po uvolnění uzamčeného zavaděče. Poté nakonfigurujte explicitně Vstupně/Výstupní proudy, vytvořené během inicializace s vlastním národním prostředím.

Překážky pro diagnostiku

V některých případech je obtížné zjistit zdroj vzájemného zablokování. Následující pododdíly projednávají tyto scénáře a způsoby řešení těchto problémů.

Implementace v záhlaví

Implementace funkce uvnitř záhlaví souborů vyberte případů může zkomplikovat diagnózu. Vložené funkce a šablonový kód vyžadují, aby byla funkce specifikována v souboru hlaviček. Jazyk C++ určuje jedno definiční pravidlo, které vynutí všechny implementace funkcí se stejným název jako sémanticky rovnocenné. V důsledku toho neprovede propojovací program jazyka C++ všechny důležité úvahy při slučování objektu souborů, které mají duplicitní implamentace v dané funkci.

Ve Visual C++ .NET a Visual C++ .NET 2003, vybere propojovací program jednoduše největší z těchto sémanticky rovnocenných definic, k přizpůsobbení dopředných deklarací a scénářů při použití volby rozdílné optimalizace pro různé zdrojové soubory. Tím se vytvoří problém pro smíšené nativní/.NET knihovny DLL.

Vzhledem k tomu, že může být stejná hlavička vložena jak do CPP souborů s povoleným nebo nepovoleným**/clr** nebo #include může být zabaleno uvnitř bloku #pragma unmanaged, je možné mít jak jazyk MSIL a nativní verze funkcí, které poskytují implementaci v hlavičkách. Jazyk MSIL a nativní implementace mají rozdílnou sémantiku s ohledem na inicializaci pod uzamčeným zavaděčem, což účinně porušuje pravidlo jedné definice. V důsledku toho, když propojovací program vybere největší implementaci, může být zvolena funkce verze jazyka MSIL i v případě, že byla explicitně zkompilována v nativním kódu, jinak než použitím směrnice #pragma unmanaged. Chcete-li zajistit, aby verze jazyka MSIL šablony nebo vložené funkce nebude nikdy volána při uzamčeném zavaděči, musí být každá definice každé funkce volána při uzamčeném zavaděči upravena se směrnicí #pragma unmanaged. Pokud je soubor hlaviček z třetí strany, nejjednodušším způsobem, jak toho dosáhnout je vložit a vyjmout směrnice #pragma unmanaged míto směrnice #include pro problematický soubor hlaviček. (Příklad naleznete v tématu managed, unmanaged.) Avšak tato strategie nebude fungovat pro hlavičky, které obsahují jiný kód, který musí volat přímo rozhraní .NET API.

Jako pohodlí pro uživatele zabývají zámek zavaděče vytvořeném propojovacím zvolí nativní implementace spravovaných při předložení i přes. Tím je zabráněno výše uvedeným problémům. Avšak existují dvě výjimky z tohoto pravidlo v této verzi, způsobené dvěmi nevyřešenými problémy s překladačem:

  • Volání vložené funkce je prostřednictvím ukazatele globální statické funkce. Tento scénář je zvláště důležitý, protože jsou volány virtuální funkce prostřednictvím ukazatelů globální funkce. Příklad:
#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();
}
  • S kompilací cílenou na Itanium je více chyb v implementaci všech ukazatelů funkce. V části kódu, uvedeném výše, pokud bylo myObject_p definováno místně uvnitř during_loaderlock(), může volání přeložit ke spravované implementaci.

Diagnostika v režimu ladění

Všechny diagnostiky problémů uzamčeného zavaděče mohou být vyřešeny v sestavení Ladění. Vydané verze sestavení nemohou být diagnostikovány a optimalizace, provedené v režimu Ladění, mohou maskovat některé MSIL ve scénáři uzamčení zavaděče.

Postup při ladění problémů uzamčeného zavaděče

Diagnostika, kterou generuje modul CLR při vyvolání funkce jazyka MSIL, způsobí pozastavení provádění modulu CLR. Naopak způsobí debugger Visual C++ kombinovaný režim pozastavení i při spuštění debuggee v procesu. Avšak při připojení do procesu není možné získat spravované callstack pro ladění, použitím kombinovaného ladícího programu.

K určení specifické funkce jazyka MSIL, která byla volána při uzamčeném zavaděči, by měli vývojáři provést následující kroky:

  1. Ujistěte se, že jsou k dispozici symboly pro mscoree.dll a mscorwks.dll.

    To lze udělat dvěma způsoby. První, PDB pro mscoree.dll a mscorwks.dll lze přidat do cesty vyhledávání symbolu. Chcete-li to provést, otevřete dialogové okno Možnosti cesty vyhledávání symbolu. (Z nabídky Nástroje klikněte na příkaz Možnosti. V levém podokně dialogového okna Možnosti otevřete uzel Ladění a klikněte na Symboly.) Přidejte cestu k PDB souborům mscoree.dll a mscorwks.dll do seznamu vyhledávání. Tyto PDB jsou nainstalovány do %VSINSTALLDIR%\SDK\v2.0\symbols. Klepněte na tlačítko OK.

    Druhý, PDB pro mscoree.dll a mscorwks.dll mohou být stáhnuty z Microsoft Symbol Server. Chcete-li nastavit Symbol Server, otevřete dialogové okno Možnosti cesty vyhledávání symbolu. (Z nabídky Nástroje klikněte na příkaz Možnosti. V levém podokně dialogového okna Možnosti otevřete uzel Ladění a klikněte na Symboly.) Přidejte následující cestu vyhledávání do seznamu vyhledávání: http://msdl.microsoft.com/download/symbols. Přidejte adresář pro mezipaměť symbolů do textového pole mezipaměť pro symbol server. Klepněte na tlačítko OK.

  2. Nastavte režim ladění na nativní režim.

    Chcete-li to provést, otevřete mřížku Vlastnosti pro projekt při spuštění v řešení. Ve skupinovém rámečku Vlastnosti konfigurace vyberte uzel Ladění. Nastavte typ pole Ladění na Nativní.

  3. Spusťte ladicí program (F5).

  4. Když je /clr generována diagnosticka, klikněte na tlačítko Opakovat a klikněte na tlačítko Konec.

  5. Otevřete okno zásobníku volání. (Z nabídky Ladění, klikněte na Windows a poté na Zásobník volání.) Pokud je problematický DllMain nebo statický inicializátor je označen zelenou šipkou. Pokud není problematická funkce určena, musí být přijaty následující kroky k vyhledání.

  6. Otevřete Aktuální okno (Z nabídky Ladění klikněte na Windows a poté na Aktuální.)

  7. Do Aktuální okna napište .load sos.dll k načtení SOS ladící služby.

  8. Napište !dumpstack do Aktuálního okna k získání úplného seznamu vnitřního zásobníku /clr.

  9. Najděte první instanci (nejblíže k dolní části zásobníku) buď _CorDllMain (pokud DllMain způsobilo problém) nebo _VTableBootstrapThunkInitHelperStub nebo GetTargetForVTableEntry (pokud je problémem statický inicializátor). Záznam zásobníku pod tímto voláním je vyvolán funkcí, implementovanou jazykem MSIL, která se pokusila provést při uzamčeném zavaděči.

  10. Přejděte do zdrojového soubor a číslo řádku, identifikované v Kroku 9 a vyřešte problém pomocí scénářů a řešení, popsanými v oddíle Scénáře.

Příklad

Popis

Následující ukázka ukazuje jak se vyhnout přesunutí kódu uzamčení zavaděče z DllMain do konstruktoru globálního objektu.

V tomto příkladu je globální spravovaný objekt, jehož konstruktor obsahuje spravovaný objekt, který byl původně v DllMain. Druhá část v tomto příkladu odkazuje na sestavení, vytvoření instance spravovaného objektu k vyvolání modulu konstruktoru, který provede inicializaci.

Kód

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

Příklad

Kód

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

Výsledek

Module ctor initializing based on global instance of class.

Test called so linker does not throw away unused object.

Viz také

Koncepty

Smíšená (nativní a spravovaná) sestavení