Inicializace smíšených sestavení
Vývojáři Systému Windows musí být při spouštění kódu vždy opatrní zámkem zavaděče během DllMain
. Při řešení sestavení ve smíšeném režimu C++/CLI je však potřeba zvážit některé další problémy.
Kód v knihovně DllMain nesmí přistupovat k modulu CLR (Common Language Runtime). To znamená, že DllMain
by nemělo provádět žá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 v rámci DllMain
žádného uvolňování paměti nebo automatického načítání knihovny by se nemělo provádět žádné uvolňování paměti ani automatické načítání knihovny .
Příčiny zámku zavaděče
Při zavedení platformy .NET existují dva různé mechanismy pro načtení spouštěcího modulu (EXE nebo DLL): jeden pro Windows, který se používá pro nespravované moduly, a jeden pro CLR, který načítá sestavení .NET. Problém se smíšeným načítáním knihovny DLL je středem kolem zavaděče operačního systému Microsoft Windows.
Pokud je do procesu načteno sestavení obsahující pouze konstruktor .NET, může zavaděč CLR provádět všechny potřebné úlohy načítání a inicializace samotné. Chcete-li však načíst smíšená sestavení, která mohou obsahovat nativní kód a data, musí být použit také zavaděč systému Windows.
Zavaděč systému Windows zaručuje, že před inicializacem není k kódu nebo datům v této knihovně DLL přístup žádný kód. A zajišťuje, že žádný kód nemůže redundantně načíst knihovnu DLL, zatímco je částečně inicializována. K tomu zavaděč systému Windows používá oddíl kritického procesu (často označovaný jako "zámek zavaděče"), který brání nebezpečnému přístupu během inicializace modulu. V důsledku toho je proces načítání zranitelný vůči mnoha klasickým scénářům vzájemného zablokování. U smíšených sestavení zvyšují následující dva scénáře riziko zablokování:
Za prvé, pokud se uživatelé pokusí spouštět funkce zkompilované do jazyka MSIL (Microsoft Intermediate Language), když je uzamčen zavaděč (například ze
DllMain
statických inicializátorů nebo v něm), může to způsobit zablokování. Vezměte v úvahu případ, kdy funkce MSIL odkazuje na typ v sestavení, které ještě není načteno. MODUL CLR se pokusí toto sestavení automaticky načíst, což může vyžadovat, aby zavaděč systému Windows zablokoval zámek zavaděče. Dojde k zablokování, protože zámek zavaděče je již uložen kódem dříve v posloupnosti volání. Provádění knihovny MSIL v rámci zámku zavaděče však nezaručuje, že dojde k zablokování. Díky tomu je tento scénář obtížné diagnostikovat a opravit. V některých případech, například když knihovna DLL odkazovaného typu neobsahuje žádné nativní konstrukty a všechny jeho závislosti neobsahují žádné nativní konstrukty, zavaděč Windows není nutné načíst sestavení .NET odkazovaného typu. Požadované sestavení nebo smíšené nativní závislosti/závislosti .NET už mohly být načteny jiným kódem. V důsledku toho může být zablokování obtížné předpovědět a může se lišit v závislosti na konfiguraci cílového počítače.Za druhé, při načítání knihoven DLL ve verzích 1.0 a 1.1 rozhraní .NET Framework modul CLR předpokládal, že zámek zavaděče nebyl uložen, a provedl několik akcí, které jsou neplatné pod zámkem zavaděče. Za předpokladu, že zámek zavaděče není uložený, je platným předpokladem čistě knihoven DLL .NET. Vzhledem k tomu, že smíšené knihovny DLL spouštějí nativní inicializační rutiny, vyžadují nativní zavaděč systému Windows a následně zámek zavaděče. Takže i když se vývojář během inicializace knihovny DLL nepokoušel spouštět žádné funkce MSIL, stále existuje malá možnost nedeterministické zablokování v rozhraní .NET Framework verze 1.0 a 1.1.
Veškerý ne determinismus byl odebrán z procesu načítání smíšené knihovny DLL. Provedli jsme ho těmito změnami:
CLR už nepravdivé předpoklady při načítání smíšených knihoven DLL.
Nespravovaná a spravovaná inicializace se provádí ve dvou samostatných a odlišných fázích. Nespravovaná inicializace probíhá nejprve (prostřednictvím
DllMain
) a spravovaná inicializace probíhá poté prostřednictvím . Konstrukce podporovaná platformou.cctor
NET Ten je pro uživatele zcela transparentní, pokud/Zl
se nepoužívá nebo/NODEFAULTLIB
nepoužívá. Další informace najdete v tématech/NODEFAULTLIB
(Ignorování knihoven) a/Zl
(Vynechání výchozího názvu knihovny).
Zámek zavaděče může stále nastat, ale nyní se opakuje a je zjištěn. Pokud DllMain
obsahuje pokyny jazyka MSIL, kompilátor vygeneruje upozornění kompilátoru (úroveň 1) C4747. Kromě toho se CRT nebo CLR pokusí zjistit a hlásit pokusy o spuštění jazyka MSIL pod zámkem zavaděče. Výsledkem detekce CRT je chyba za běhu modulu runtime C R6033.
Zbytek tohoto článku popisuje zbývající scénáře, pro které může knihovna MSIL provést pod zámkem zavaděče. Ukazuje, jak tento problém vyřešit v jednotlivých scénářích a technikách ladění.
Scénáře a alternativní řešení
Existuje několik různých situací, kdy uživatelský kód může spustit jazyk MSIL pod zámkem zavaděče. Vývojář musí zajistit, aby se implementace uživatelského kódu v každém z těchto okolností nepokoušla spouštět pokyny jazyka MSIL. Následující pododdíly popisují všechny možnosti s diskuzí o řešení problémů v nejběžnějších případech.
DllMain
Funkce DllMain
je uživatelem definovaný vstupní bod pro knihovnu DLL. Pokud uživatel neurčí jinak, DllMain
vyvolá se při každém připojení procesu nebo vlákna k obsahující knihovně DLL nebo odpojení od něj. Vzhledem k tomu, že k tomuto vyvolání může dojít, když je uzamčen zavaděč uložen, neměla by být do knihovny MSIL zkompilována žádná funkce zadaná DllMain
uživatelem. Kromě toho nelze do knihovny MSIL zkompilovat žádnou funkci v kořenovém DllMain
stromu volání. Pokud chcete tyto problémy vyřešit, blok kódu, který definuje DllMain
, by se měl upravit pomocí #pragma unmanaged
. Totéž by se mělo provést pro každou funkci, která DllMain
volá.
V případech, kdy tyto funkce musí volat funkci, která vyžaduje implementaci jazyka MSIL pro jiné kontexty volání, můžete použít strategii duplikace, ve které se vytvoří rozhraní .NET i nativní verze stejné funkce.
Jako alternativu můžete DllMain
odebrat implementaci poskytovanou uživatelem, pokud není nutná nebo pokud ji není nutné spouštět pod zámkem zavaděče, můžete odebrat uživatelsky poskytnutou DllMain
implementaci, která eliminuje problém.
Pokud DllMain
se pokusí spustit jazyk MSIL přímo, výsledkem bude upozornění kompilátoru (úroveň 1) C4747 . Kompilátor ale nedokáže rozpoznat případy, kdy DllMain
se volá funkce v jiném modulu, který se zase pokusí spustit jazyk MSIL.
Další informace o tomto scénáři naleznete v tématu Impediments to Diagnosticss.
Inicializace statických objektů
Inicializace statických objektů může vést k zablokování, pokud je vyžadován dynamický inicializátor. Jednoduché případy (například když přiřadíte hodnotu známou v době kompilace statické proměnné) nevyžadují dynamickou inicializaci, takže neexistuje žádné riziko zablokování. Některé statické proměnné se ale inicializují voláním funkce, vyvoláním konstruktoru nebo výrazy, které nelze vyhodnotit v době kompilace. Všechny tyto proměnné vyžadují, aby se kód spustil během inicializace modulu.
Následující kód ukazuje příklady statických inicializátorů, které vyžadují dynamickou inicializaci: volání funkce, konstrukce objektu a inicializace ukazatele. (Tyto příklady nejsou statické, ale předpokládají se, že mají definice v globálním oboru, které mají stejný účinek.)
// dynamic initializer function generated
int a = init();
CObject o(arg1, arg2);
CObject* op = new CObject(arg1, arg2);
Toto riziko zablokování závisí na tom, jestli je obsahující modul zkompilován /clr
a zda bude spuštěna knihovna MSIL. Konkrétně platí, že pokud je statická proměnná zkompilována bez /clr
(nebo je v #pragma unmanaged
bloku) a dynamický inicializátor potřebný k inicializaci vede ke spuštění instrukcí jazyka MSIL, může dojít k zablokování. Je to proto, že pro moduly zkompilované bez /clr
, inicializace statických proměnných provádí DllMain. Naproti tomu statické proměnné zkompilované pomocí /clr
inicializace .cctor
, po dokončení nespravované inicializační fáze a zavaděč je uvolněn zámek zavaděče.
Existuje řada řešení pro vzájemné zablokování způsobené dynamickou inicializací statických proměnných. Jsou zde uspořádány zhruba v pořadí času potřebném k vyřešení problému:
Zdrojový soubor obsahující statickou proměnnou lze zkompilovat pomocí
/clr
.Všechny funkce volané statickou proměnnou lze zkompilovat do nativního kódu pomocí direktivy
#pragma unmanaged
.Ručně naklonujte kód, na který statická proměnná závisí, a to tím, že poskytuje rozhraní .NET i nativní verzi s různými názvy. Vývojáři pak mohou volat nativní verzi z nativních statických inicializátorů a volat verzi .NET jinde.
Uživatelem zadané funkce ovlivňující spuštění
Existuje několik funkcí zadaných uživatelem, na kterých knihovny závisí při inicializaci během spouštění. Například při globálním přetížení operátorů v jazyce C++, jako new
jsou například a delete
operátory, se verze poskytované uživatelem používají všude, včetně inicializace a zničení standardní knihovny jazyka C++. V důsledku toho standardní knihovna jazyka C++ a statická inicializátory poskytované uživatelem vyvolá všechny uživatelsky poskytnuté verze těchto operátorů.
Pokud jsou verze poskytnuté uživatelem zkompilovány do jazyka MSIL, pokusí se tyto inicializátory spustit pokyny jazyka MSIL, zatímco se zamyká zavaděč. Zadaný uživatel malloc
má stejné důsledky. Chcete-li tento problém vyřešit, musí být některé z těchto přetížení nebo uživatelsky zadaných definic implementovány jako nativní kód pomocí direktivy #pragma unmanaged
.
Další informace o tomto scénáři naleznete v tématu Impediments to Diagnosticss.
Vlastní národní prostředí
Pokud uživatel poskytuje vlastní globální národní prostředí, použije se toto národní prostředí k inicializaci všech budoucích vstupně-výstupních datových proudů, včetně streamů, které jsou staticky inicializovány. Pokud je tento globální objekt národního prostředí zkompilován do jazyka MSIL, mohou být vyvolány členské funkce národního prostředí kompilované do jazyka MSIL, zatímco je uzamčen zavaděč.
Existují tři možnosti řešení tohoto problému:
Zdrojové soubory obsahující všechny definice globálních vstupně-výstupních proudů je možné zkompilovat pomocí této /clr
možnosti. Zabrání spuštění statických inicializátorů pod zámkem zavaděče.
Definice vlastních funkcí národního prostředí lze zkompilovat do nativního kódu pomocí direktivy #pragma unmanaged
.
Nepoužívejte nastavení vlastního národního prostředí jako globálního národního prostředí, dokud se zámek zavaděče nevyvolá. Pak explicitně nakonfigurujte vstupně-výstupní datové proudy vytvořené během inicializace pomocí vlastního národního prostředí.
Překážky diagnostiky
V některých případech je obtížné rozpoznat zdroj zablokování. V následujících dílčích oddílech jsou tyto scénáře a způsoby, jak tyto problémy obejít.
Implementace v hlavičkách
Ve vybraných případech můžou implementace funkcí uvnitř souborů hlaviček komplikovat diagnostiku. Vložené funkce i kód šablony vyžadují, aby funkce byly zadány v souboru hlaviček. Jazyk C++ určuje pravidlo one definition, které vynutí, aby všechny implementace funkcí se stejným názvem byly sémanticky ekvivalentní. Proto linker jazyka C++ nemusí při slučování souborů objektů, které mají duplicitní implementace dané funkce, vzít v úvahu žádné zvláštní aspekty.
Ve verzích sady Visual Studio před sadou Visual Studio 2005 linker jednoduše zvolí největší z těchto sémanticky ekvivalentních definic. Provádí se tak, aby vyhovoval předávací deklaraci a scénáře, kdy se pro různé zdrojové soubory používají různé možnosti optimalizace. Vytvoří problém pro smíšené nativní knihovny DLL a knihovny DLL .NET.
Vzhledem k tomu, že stejnou hlavičku můžou obsahovat soubory C++ s povoleným i zakázaným /clr
nebo #include lze zabalit do #pragma unmanaged
bloku, je možné mít jazyk MSIL i nativní verze funkcí, které poskytují implementace v hlavicích. Rozhraní MSIL a nativní implementace mají různé sémantiky pro inicializaci pod zámkem zavaděče, což účinně porušuje jedno pravidlo definice. V důsledku toho, když linker zvolí největší implementaci, může zvolit verzi jazyka MSIL funkce, i když byla explicitně zkompilována do nativního kódu jinde používající direktivy #pragma unmanaged
. Aby se zajistilo, že verze knihovny MSIL šablony nebo vložené funkce není nikdy volána pod zámkem zavaděče, musí být každá definice každé takové funkce volaná pod zámkem zavaděče upravena direktivou #pragma unmanaged
. Pokud je soubor hlaviček od třetí strany, nejjednodušší způsob, jak tuto změnu provést, je nasdílení a otevření direktivy #pragma unmanaged
týkající se direktivy #include pro soubor hlavičky off-end. (Příklad je spravovaný, nespravovaný .) Tato strategie ale nefunguje pro hlavičky, které obsahují jiný kód, který musí přímo volat rozhraní .NET API.
Jako pohodlí pro uživatele, kteří se zabývají zámkem zavaděče, bude linker zvolit nativní implementaci přes spravovanou, když se zobrazí obojí. Ve výchozím nastavení se vyhnete výše uvedeným problémům. V této verzi ale existují dvě výjimky z důvodu dvou nevyřešených problémů s kompilátorem:
- Volání vložené funkce je prostřednictvím globálního ukazatele statické funkce. Tento scénář není tabulka, protože virtuální funkce se volají prostřednictvím globálních ukazatelů funkcí. 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();
}
Diagnostika v režimu ladění
Všechny diagnostiky problémů se zámkem zavaděče by se měly provádět pomocí sestavení ladění. Sestavení vydaných verzí nemusí vytvářet diagnostiku. Optimalizace provedené v režimu vydání můžou maskovat některé scénáře uzamčení zavaděče msIL.
Ladění problémů se zámkem zavaděče
Diagnostika, kterou CLR vygeneruje při vyvolání funkce MSIL, způsobí, že CLR pozastaví provádění. To zase způsobí, že ladicí program ve smíšeném režimu Visual C++ se pozastaví i při spuštění ladicího programu v procesu. Při připojování k procesu ale není možné získat spravovaný balíček volání pro ladicí program pomocí smíšeného ladicího programu.
Aby vývojáři identifikovali konkrétní funkci MSIL, která byla volána pod zámkem zavaděče, měli by provést následující kroky:
Ujistěte se, že jsou k dispozici symboly pro mscoree.dll a mscorwks.dll.
Symboly můžete zpřístupnit dvěma způsoby. Nejprve je možné do cesty hledání symbolů přidat soubory PDB pro mscoree.dll a mscorwks.dll. Pokud je chcete přidat, otevřete dialogové okno možností cesty hledání symbolů. (Z Nabídka Nástroje , zvolte Možnosti. V levém podokně dialogového okna Možnosti otevřete uzel Ladění a zvolte Symboly.) Do seznamu hledání přidejte cestu k mscoree.dll a mscorwks.dll souborů PDB. Tyto soubory PDB jsou nainstalovány do symbolů %VSINSTALLDIR%\SDK\v2.0\. Vyberte OK.
Za druhé, soubory PDB pro mscoree.dll a mscorwks.dll lze stáhnout ze serveru symbolů Společnosti Microsoft. Chcete-li konfigurovat server symbolů, otevřete dialogové okno možností cesty hledání symbolů. (Z Nabídka Nástroje , zvolte Možnosti. V levém podokně dialogového okna Možnosti otevřete uzel Ladění a zvolte Symboly.) Přidejte tuto cestu hledání do seznamu hledání:
https://msdl.microsoft.com/download/symbols
. Přidejte adresář mezipaměti symbolů do textového pole mezipaměti serveru symbolů. Vyberte OK.Nastavte režim ladicího programu na režim pouze nativní.
Otevřete mřížku Vlastnosti pro spouštěný projekt v řešení. Vyberte Ladění vlastností>konfigurace. Nastavte vlastnost Typ ladicího programu na Native-Only.
Spusťte ladicí program (F5).
/clr
Po vygenerování diagnostiky zvolte Opakovat a pak zvolte Přerušení.Otevřete okno zásobníku volání. (Na řádku nabídek zvolte Ladění>zásobníku volání systému Windows>.) Urážka
DllMain
nebo statická inicializátor se identifikuje zelenou šipkou. Pokud není identifikovaná funkce pro odsunutí, je potřeba k jeho nalezení provést následující kroky.Otevřete okno Okamžité (na řádku nabídek zvolte Ladit>Windows>Immediate.)
Zadejte
.load sos.dll
do okna Okamžité načtení služby ladění SOS.Zadáním
!dumpstack
do příkazového okna získáte úplný výpis interního/clr
zásobníku.Vyhledejte první instanci (nejblíže ke konci zásobníku) _CorDllMain (pokud
DllMain
problém způsobuje) nebo _VTableBootstrapThunkInitHelperStub nebo GetTargetForVTableEntry (pokud problém způsobuje statická inicializátor). Položka zásobníku těsně pod tímto voláním je vyvolání implementované funkce MSIL, která se pokusila provést pod zámkem zavaděče.Přejděte ke zdrojovému souboru a číslu řádku zjištěnému v předchozím kroku a opravte problém pomocí scénářů a řešení popsaných v části Scénáře.
Příklad
Popis
Následující ukázka ukazuje, jak se vyhnout zámku zavaděče přesunutím kódu z DllMain
konstruktoru globálního objektu.
V této ukázce existuje globální spravovaný objekt, jehož konstruktor obsahuje spravovaný objekt, který byl původně v DllMain
. Druhá část této ukázky odkazuje na sestavení a vytvoří instanci spravovaného objektu pro vyvolání konstruktoru modulu, který inicializaci provede.
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 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;
}
Tento příklad ukazuje problémy s inicializací smíšených sestavení:
// 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ýsledkem tohoto kódu je následující výstup:
Module ctor initializing based on global instance of class.
Test called so linker doesn't throw away unused object.