Sdílet prostřednictvím


Inicializace smíšených sestavení

Vývojáři systému Windows by měli být při spouštění kódu vždy opatrní na zámek 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 ani nepřímo; žádný spravovaný kód by neměl být deklarován ani implementován v DllMain; a v rámci DllMain by se nemělo provádět žádné uvolňování paměti ani automatické načítání knihoven.

Příčiny uzamčení zavaděčem

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 DLL se soustředí kolem zavaděče operačního systému Microsoft Windows.

Pokud je do procesu načteno sestavení obsahující pouze konstrukty .NET, může zavaděč CLR provádět všechny potřebné úlohy načítání a inicializace sám. 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 inicializací nemá žádný kód přístup ke kódu nebo datům v této knihovně DLL. 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á globálně kritickou sekci (často označovanou 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 Microsoft Intermediate Language (MSIL), když je zámek nakladače držen (například z DllMain nebo ve statických inicializátorech), může to vést k deadlocku. 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. K zablokování dojde, protože zámek zavaděče je již držen kódem dříve v posloupnosti volání. Provádění MSIL během 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 její závislosti neobsahují žádné nativní konstrukty, zavaděč Windows nemusí načítat sestavení .NET tohoto typu. Požadované sestavení nebo nativní/.NET smíšené závislosti mohly již 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 držen, a provedl několik akcí, které nejsou platné, když je zámek zavaděče držen. 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ý nedeterminismus byl odstraněn z procesu načítání smíšených knihoven DLL. Provedli jsme ho těmito změnami:

  • CLR již nedělá liché 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 platformou .NET podporované konstrukce .cctor. Ten je pro uživatele zcela transparentní, pokud se nevyužívají /Zl nebo /NODEFAULTLIB. 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 vyskytuje opakovaně a je detekován. Pokud DllMain obsahuje pokyny jazyka MSIL, kompilátor vygeneruje upozornění kompilátoru (úroveň 1) C4747. Kromě toho se buď CRT, nebo CLR pokusí detekovat a oznámit pokusy o spuštění kódu MSIL pod zámkem zavaděče. Výsledek detekce CRT vede k diagnostické chybě za běhu C Run-Time Error R6033.

Zbytek tohoto článku popisuje zbylé scénáře, pro které může MSIL běžet 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 MSIL během zámku 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 držen zámek zavaděče, žádná uživatelem zadaná funkce by neměla být zkompilována do MSIL. Kromě toho nelze žádnou funkci ve stromu volání zakořeněném v DllMain zkompilovat do MSIL. 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.

Alternativně, pokud DllMain není nutné, nebo pokud se nemá spouštět pod zámkem zavaděče, můžete odebrat uživatelsky poskytnutou implementaci DllMain, což 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 volá funkci v jiném modulu, která se pak pokusí spustit jazyk MSIL.

Další informace o tomto scénáři naleznete v části Impediments to Diagnosis.

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é s /clr jsou inicializovány pomocí .cctor poté, co byla dokončena nespravovaná inicializační fáze a zámek zavaděče byl uvolněn.

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 .

  • Manuálně naklonujte kód, na kterém závisí statická proměnná, a vytvořte jak .NET verzi, tak i nativní verzi s různými názvy. Vývojáři pak mohou volat nativní verzi prostřednictvím statických inicializátorů jazyka a verzi .NET využívat 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 C++, jako jsou operátory new a delete, se uživatelsky definované verze používají všude, včetně inicializace a destrukce v rámci standardní knihovny 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, budou se tyto inicializátory pokoušet spustit pokyny jazyka MSIL, zatímco je zavaděč uzamčen. Uživatelem dodaný 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 Diagnosis.

Vlastní lokalizace

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í lokalizační objekt zkompilován do jazyka MSIL, pak mohou být při uzamčení zavaděče vyvolány členské funkce tohoto objektu, které jsou rovněž zkompilovány do jazyka MSIL.

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 neuvolní. 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 pododdílech se diskutují tyto scénáře a možnosti, 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 to, aby vyhovovaly dopředným deklaracím a scénářům, kdy se pro různé zdrojové soubory používají různé optimalizační možnosti. Vytvoří problém pro smíšené nativní knihovny DLL a knihovny DLL .NET.

Vzhledem k tomu, že stejný hlavičkový soubor může obsahovat soubory C++ s povoleným i zakázaným /clr nebo může být #include umístěn do bloku #pragma unmanaged, je možné mít verze funkcí ve formátu MSIL i nativním formátu, které poskytují implementace v hlavičkových souborech. MSIL a nativní implementace mají různou sémantiku pro inicializaci při používání zámku zavaděče, což účinně porušuje pravidlo jedné definice. V důsledku toho, když linker zvolí nejrozsáhlejší implementaci, může zvolit MSIL verzi funkce, i když byla explicitně zkompilována do nativního kódu jinde s použitím direktivy #pragma unmanaged. Aby se zajistilo, že verze MSIL šablony nebo inline funkce není nikdy volána pod zámkem zavaděče, musí být každá definice těchto funkcí upravena pomocí direktivy #pragma unmanaged. Pokud je soubor hlaviček od třetí strany, nejjednodušší způsob, jak tuto změnu provést, je použít push a pop direktivu #pragma unmanaged kolem direktivy #include pro problemový soubor hlavičky. (Viz spravovaný, nespravovaný pro příklad.) Tato strategie ale nefunguje pro hlavičky, které obsahují jiný kód a musí přímo volat .NET API.

Pro usnadnění uživatelům, kteří se zabývají zámkem zavaděče, překladač zvolí nativní implementaci nad spravovanou, když jsou k dispozici obě možnosti. 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í inline funkce probíhá prostřednictvím ukazatele globální statické funkce. Tento scénář není možný, 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í v režimu uvolněné kompilace nemusí produkovat diagnostiku. Optimalizace provedené v režimu Release 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 se pozastaví ladicí program ve smíšeném režimu Visual C++ i při spuštění laděného programu v rámci 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:

  1. 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ídky 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 souborům PDB pro mscoree.dll a mscorwks.dll. 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.

  2. Nastavte režim ladicího programu na pouze nativní režim.

    Otevřete mřížku Vlastnosti pro spouštěný projekt v řešení. Vyberte Vlastnosti konfigurace>Ladění. Nastavte vlastnost Typ ladicího programu na Native-Only.

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

  4. /clr Po vygenerování diagnostiky zvolte Opakovat a pak zvolte Přerušení.

  5. Otevřete okno zásobníku volání. (Na řádku nabídek zvolte Ladění>Okna>Zásobník volání.) Problémový DllMain nebo statický inicializátor je označen zelenou šipkou. Pokud není identifikována problémová funkce, je potřeba k jejímu nalezení provést následující kroky.

  6. Otevřete okno Okamžité (na řádku nabídek zvolte Ladit>>.)

  7. Zadejte .load sos.dll do okna Okamžité pro načtení služby ladění SOS.

  8. Zadáním !dumpstack do okna Okamžitě získáte úplný výpis interního /clr zásobníku.

  9. Vyhledejte první instanci (nejblíže ke konci zásobníku prvku) _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.

  10. 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.

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

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.

Viz také

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