Vegyes összeállítások inicializálása

A Windows-fejlesztőknek mindig óvatosnak kell lenniük a rakodózárral a kód futtatásakor.DllMain A C++/CLI vegyes módú szerelvények kezelésekor azonban további problémákat is figyelembe kell venni.

A DllMain kódjának nem szabad hozzáférnie a .NET Common Language Runtime (CLR) modulhoz. Ez azt jelenti, hogy DllMain nem kell közvetlenül vagy közvetve meghívni a felügyelt függvényeket; nem szabad felügyelt kódot deklarálni vagy implementálni DllMain; és nem szabad szemétgyűjtést vagy automatikus tárbetöltést végezni a rendszeren belül DllMain.

A betöltési zár okai

A .NET platform bevezetésével két különböző mechanizmus létezik egy végrehajtási modul (EXE vagy DLL) betöltésére: az egyik a Windowshoz készült, amelyet nem felügyelt modulokhoz használnak, egyet pedig a CLR-hez, amely betölti a .NET-szerelvényeket. A vegyes DLL betöltési probléma a Microsoft Windows operációs rendszer betöltője körül összpontosul.

Ha egy csak .NET-szerkezeteket tartalmazó szerelvényt tölt be egy folyamatba, a CLR-betöltő elvégezheti az összes szükséges betöltési és inicializálási feladatot. A natív kódot és adatokat tartalmazó vegyes szerelvények betöltéséhez azonban a Windows-betöltőt is használni kell.

A Windows-betöltő garantálja, hogy az inicializálás előtt egyetlen kód sem fér hozzá az adott DLL-ben lévő kódhoz vagy adatokhoz. Ez biztosítja, hogy egyetlen kód sem töltheti be redundánsan a DLL-t részleges inicializálás közben. Ehhez a Windows-betöltő egy folyamatszintű kritikus szakaszt (gyakran "betöltőzár" néven) használ, amely megakadályozza a nem biztonságos hozzáférést a modul inicializálása során. Ennek eredményeképpen a betöltési folyamat számos klasszikus holtpont-forgatókönyvnek van kitéve. Vegyes szerelvények esetén a következő két forgatókönyv növeli a holtpont kockázatát:

  • Először is, ha a felhasználók megpróbálják végrehajtani a Microsoft köztes nyelvére (MSIL) lefordított függvényeket, amikor a betöltőzár érvényes (például DllMain-ból vagy statikus inicializálókban), az holtpontot okozhat. Vegye figyelembe azt az esetet, amikor az MSIL függvény egy még nem betöltött szerelvény típusára hivatkozik. A CLR megpróbálja automatikusan betölteni a szerelvényt, ami miatt előfordulhat, hogy a Windows rakodónak blokkolnia kell a rakodózárat. Holtpont lép fel, mivel a betöltőzárat már korábban kód tartja a hívássorozatban. Az MSIL rakodózár alatt történő végrehajtása azonban nem garantálja, hogy holtpont jön létre. Ez az, ami megnehezíti ezt a forgatókönyvet a diagnosztizálásban és a javításban. Bizonyos körülmények között, például ha a hivatkozott típus DLL-je nem tartalmaz natív szerkezeteket, és minden függősége nem tartalmaz natív szerkezeteket, a Windows-betöltő nem szükséges a hivatkozott típus .NET-szerelvényének betöltéséhez. Emellett előfordulhat, hogy a szükséges szerelvényt vagy annak vegyes natív/.NET-függőségeit más kód már betöltötte. Ennek következtében a holtpontot nehéz megjósolni, és a célgép konfigurációjától függően változhat.

  • Másodszor, amikor a .NET-keretrendszer 1.0-s és 1.1-s verziójában DLL-eket tölt be, a CLR feltételezte, hogy a betöltési zár nincs fogva, és több olyan műveletet hajtott végre, amelyek érvénytelenek a betöltési zár alatt. Feltételezve, hogy a betöltőzár nincs fogva, érvényes feltételezés a tisztán .NET DLL-ek esetében. Mivel azonban a vegyes DLL-ek natív inicializálási rutinokat hajtanak végre, a natív Windows-betöltőre és következésképpen a betöltő zárolására van szükség. Így még ha a fejlesztő nem is kísérelt meg MSIL-függvényeket végrehajtani a DLL inicializálása során, a .NET-keretrendszer 1.0-s és 1.1-es verzióiban továbbra is fennállt a nemdeterminisztikus holtpont lehetősége.

Minden nem determinizmus el lett távolítva a vegyes DLL betöltési folyamatából. Ez a következő módosításokkal valósult meg:

  • A CLR már nem tesz hamis feltételezéseket vegyes DLL-ek betöltésekor.

  • A nem felügyelt és felügyelt inicializálás két különálló és különálló fázisban történik. A nem felügyelt inicializálás először (keresztül DllMain) történik, a felügyelt inicializálás pedig utána, egy .NET által támogatott .cctor szerkezet segítségével. Az utóbbi teljesen transzparens a felhasználó számára, kivéve, ha a /Zl-t vagy a /NODEFAULTLIB-t használják. További információ:/NODEFAULTLIB (Tárak figyelmen kívül hagyása) és /Zl (Az alapértelmezett kódtárnév kihagyása).

A rakodózár továbbra is előfordulhat, de most már reprodukálható módon történik, és a rendszer észleli. Ha DllMain MSIL-utasításokat tartalmaz, a fordító figyelmeztetést generál Fordítói figyelmeztetés (1. szint) C4747. Ezenkívül vagy a CRT vagy a CLR megpróbálja észlelni és jelenteni az MSIL rakodózár alatt történő végrehajtására tett kísérleteket. A CRT-észlelés futásidejű diagnosztika során R6033-as hiba, C Run-Time hibát eredményez.

A cikk további része azokat a fennmaradó forgatókönyveket ismerteti, amelyek esetében az MSIL végrehajtható a rakodózár alatt. Bemutatja, hogyan oldhatja meg a problémát az egyes forgatókönyvekben, és ismerteti a hibakeresési technikákat.

Forgatókönyvek és kerülő megoldások

A felhasználói kód számos különböző helyzetben képes végrehajtani az MSIL-t a rakodózár alatt. A fejlesztőnek biztosítania kell, hogy a felhasználói kód implementációja ne kísérelje meg az MSIL-utasítások végrehajtását ezen körülmények között. Az alábbi alszakaszok a leggyakoribb esetek problémáinak megoldásával kapcsolatos összes lehetőséget ismertetik.

DllMain

A DllMain függvény egy DLL felhasználó által definiált belépési pontja. Ha a felhasználó másként nem rendelkezik, DllMain minden alkalommal akkor kerül meghívásra, amikor egy folyamat vagy szál csatlakozik az adott DLL-hez, vagy leválik arról. Mivel ez a hívás a betöltő zárolása közben is előfordulhat, a felhasználó által megadott DllMain függvényeket nem szabad lefordítani MSIL nyelvre. Ezenkívül a DllMain gyökerű hívásfában található egyetlen függvény sem fordítható le MSIL-re. A problémák megoldásához a definiált kódblokkot DllMain módosítani kell a következővel #pragma unmanaged: . Ugyanezt kell tenni minden olyan függvény esetében, amely DllMain hív.

Azokban az esetekben, amikor ezeknek a függvényeknek olyan függvényt kell meghívnia, amely msIL-implementációt igényel más hívókörnyezetekhez, használhat duplikálási stratégiát is, amelyben a rendszer létrehoz egy .NET-et és ugyanannak a függvénynek egy natív verzióját is.

Alternatív megoldásként, ha DllMain nincs szükség rá, vagy ha nem kell a rakodózár alatt végrehajtani, eltávolíthatja a felhasználó által biztosított DllMain implementációt, ami kiküszöböli a problémát.

Ha DllMain közvetlenül próbálja végrehajtani az MSIL-t, a fordító figyelmeztetése (1. szint) C4747 eredménye lesz. A fordító azonban nem tudja észlelni azokat az eseteket, amikor DllMain egy függvényt hív meg egy másik modulban, amely megkísérli végrehajtani az MSIL-t.

A forgatókönyvről további információt a diagnosztika akadályai című témakörben talál.

Statikus objektumok inicializálása

A statikus objektumok inicializálása holtpontot eredményezhet, ha dinamikus inicializálóra van szükség. Az egyszerű esetek (például amikor fordításkor ismert értéket rendel egy statikus változóhoz) nem igényelnek dinamikus inicializálást, így nincs esély a holtpontra. Egyes statikus változókat azonban függvényhívások, konstruktorhívások vagy olyan kifejezések inicializálnak, amelyek fordításkor nem értékelhetők ki. Ezekhez a változókhoz kód szükséges a modul inicializálása során.

Az alábbi kód példákat mutat be a dinamikus inicializálást igénylő statikus inicializálókra: függvényhívásra, objektumépítésre és mutató inicializálására. (Ezek a példák nem statikusak, de feltételezzük, hogy a globális hatókörben vannak definíciók, amelyek ugyanolyan hatással vannak.)

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

A holtpont kockázata attól függ, hogy a tartalmazó modul /clr-val van-e lefordítva, és hogy az MSIL végre lesz-e hajtva. Pontosabban, ha a statikus változót statikus inicializáló nélkül fordítják le (vagy /clr blokkban van), és az ehhez szükséges dinamikus inicializáló MSIL-utasításokat hajt végre, holtpont léphet fel. Ennek az az oka, hogy a /clr nélkül fordított modulok esetében a statikus változók inicializálását a DllMain végzi. Ezzel szemben a /clr segítségével lefordított statikus változókat a rendszer a .cctor által inicializálja, miután a nem felügyelt inicializálási fázis befejeződött és a betöltőzár feloldásra került.

A statikus változók dinamikus inicializálása számos megoldást kínál a holtpontra. Itt nagyjából a probléma megoldásához szükséges idő szerint vannak elrendezve:

  • A statikus változót tartalmazó forrásfájl a következővel /clrfordítható le: .

  • A statikus változó által hívott összes függvény lefordítható natív kódra az #pragma unmanaged irányelv használatával.

  • Manuálisan klónozza azt a kódot, amelytől a statikus változó függ, és a .NET-et és a natív verziót is különböző néven adja meg. A fejlesztők ezután meghívhatják a natív verziót natív statikus inicializálókból, és máshol is meghívhatják a .NET-verziót.

User-Supplied indítást befolyásoló függvények

Számos felhasználó által megadott függvény létezik, amelyektől a kódtárak az indítás során az inicializálástól függenek. Ha például globálisan túlterheli a C++ operátorokat, mint például a new és delete operátorokat, a felhasználó által biztosított verziókat használják mindenhol, beleértve a C++ standard könyvtár inicializálását és megsemmisítését is. Ennek eredményeképpen a C++ standard kódtár és a felhasználó által biztosított statikus inicializálók meghívják az operátorok felhasználó által biztosított verzióit.

Ha a felhasználó által biztosított verziók az MSIL-re vannak lefordítva, akkor ezek az inicializálók megpróbálják végrehajtani az MSIL-utasításokat a rakodózár megtartása közben. A felhasználó által megadott malloc következmények ugyanazok. A probléma megoldásához ezen túlterhelések vagy a felhasználó által megadott definíciók bármelyikét natív kódként kell implementálni az #pragma unmanaged irányelv használatával.

A forgatókönyvről további információt a diagnosztika akadályai című témakörben talál.

Egyéni területi beállítások

Ha a felhasználó egyéni globális területi beállítást biztosít, ez a területi beállítás az összes jövőbeli I/O-stream inicializálására szolgál, beleértve a statikusan inicializált streameket is. Ha ez a globális helyi beállítás objektum az MSIL-re van lefordítva, akkor a helyi beállítás objektumtag-függvények, amelyek szintén MSIL-re vannak lefordítva, meghívhatók a betöltő zárolása időtartama alatt.

A probléma megoldásának három lehetősége van:

Az összes globális I/O-stream definíciót tartalmazó forrásfájl a /clr beállítással fordítható le. Megakadályozza, hogy a statikus inicializálók a rakodózár alatt legyenek végrehajtva.

Az egyéni területi függvénydefiníciók az irányelv használatával #pragma unmanaged lefordíthatók natív kódra.

Ne állítsa be az egyéni területi beállítást globális területi beállításként, amíg a betöltő zárolása ki nem oldódik. Ezután explicit módon konfigurálja az inicializálás során létrehozott I/O-adatfolyamokat az egyéni területi beállítással.

A diagnosztika akadályai

Bizonyos esetekben nehéz azonosítani a holtpontok forrását. Az alábbi alszakaszok ezeket a forgatókönyveket és a problémák megoldásának módjait ismertetik.

Implementáció a fejlécekben

Bizonyos esetekben a fejlécfájlokban lévő függvény-implementációk megnehezíthetik a diagnózist. A beágyazott függvényekhez és a sablonkódokhoz egyaránt szükség van a függvények fejlécfájlban való megadására. A C++ nyelv határozza meg a One Definition Szabályt, amely szemantikailag egyenértékűre kényszeríti az azonos nevű függvények összes implementációját. Következésképpen a C++ csatolónak nem kell különösebb szempontokat figyelembe vennie egy adott függvény duplikált implementációit tartalmazó objektumfájlok egyesítésekor.

A Visual Studio 2005 előtti Visual Studio-verziókban a linker egyszerűen a legnagyobbat választja ki ezek közül a szemantikailag egyenértékű definíciók közül. Ez a művelet a továbbítási deklarációk és a különböző forrásfájlokhoz különböző optimalizálási lehetőségek használata esetén szükséges. Problémát jelent a vegyes natív és .NET DLL-ek számára.

Mivel a /clr engedélyezett és letiltott C++ fájlok ugyanazt a fejlécet is tartalmazhatják, vagy egy #include beágyazható egy #pragma unmanaged blokkba, előfordulhat, hogy mind MSIL, mind natív verziói megjelennek a fejlécekben olyan függvényeknek, amelyek implementációkat biztosítanak. Az MSIL és a natív implementációk eltérő szemantikával rendelkeznek az inicializáláshoz a betöltőzár alatt, ami hatékonyan sérti az egy definíciós szabályt. Következésképpen, amikor a linker a legnagyobb implementációt választja, akkor is kiválaszthatja egy függvény MSIL-verzióját, még akkor is, ha azt az irányelv használatával #pragma unmanaged máshol, natív kódra fordították. Annak biztosítása érdekében, hogy egy sablon vagy beágyazott függvény MSIL-verzióját soha ne hívják meg betöltőzár alatt, minden ilyen, betöltőzár alatt meghívott függvény definícióját módosítani kell az #pragma unmanaged irányelvvel. Ha a fejlécfájl harmadik féltől származik, a módosítás legegyszerűbb módja az, hogy az érintett fejlécfájl #include irányelve köré beillesztjük a #pragma unmanaged direktíva érvényesítési és visszavonási parancsait. (Lásd a felügyelt, nem felügyelt példát.) Ez a stratégia azonban nem működik olyan fejlécek esetében, amelyek más kódot tartalmaznak, amelyeknek közvetlenül kell meghívnia a .NET API-kat.

A rakodózárral foglalkozó felhasználók kényelme érdekében a linker kiválasztja a natív implementációt a felügyelt felett, ha mindkettő megjelenik. Ez az alapértelmezett beállítás elkerüli a fenti problémákat. Ebben a kiadásban azonban két kivétel van a szabály alól, mert két megoldatlan probléma merült fel a fordítóval kapcsolatban:

  • A beágyazott függvény hívása globális statikus függvénymutatón keresztül történik. Ez a forgatókönyv nem megvalósítható, mert a virtuális függvényeket globális függvénymutatókon keresztül hívják meg. Például
#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();
}

Diagnosztizálás hibakeresési módban

A rakodózárolási problémák diagnosztizálását hibakeresési buildekkel kell elvégezni. Előfordulhat, hogy a kiadási buildek nem hoznak létre diagnosztikát. A kiadási módban végzett optimalizálások elfedhetik az MSIL egy részét a rakodózárolási forgatókönyvek alatt.

A betöltő zárolási problémáinak hibakeresése

A CLR által az MSIL-függvény meghívásakor létrehozott diagnosztikával a CLR felfüggeszti a végrehajtást. Emiatt a Visual C++ vegyes módú hibakereső is felfüggesztésre kerül, ha a hibakeresőt folyamatban futtatják. A folyamathoz való csatoláskor azonban nem lehet felügyelt híváshívást beszerezni a hibakeresőhöz a vegyes hibakereső használatával.

A betöltési zár alatt hívott MSIL-függvény azonosításához a fejlesztőknek a következő lépéseket kell végrehajtaniuk:

  1. Győződjön meg arról, hogy a mscoree.dll és mscorwks.dll szimbólumai elérhetők.

    A szimbólumokat kétféleképpen teheti elérhetővé. Először a mscoree.dll és a mscorwks.dll PDB-k hozzáadhatók a szimbólumkeresési útvonalhoz. A hozzáadásukhoz nyissa meg a szimbólum keresési útvonalának beállításai párbeszédpanelt. (Az Eszközök menüben válassza a Beállítások lehetőséget. A Beállítások párbeszédpanel bal oldali ablaktábláján nyissa meg a hibakeresési csomópontot, és válassza a Szimbólumok lehetőséget.) Adja hozzá az elérési utat a mscoree.dll és mscorwks.dll PDF-fájlokat a keresési listához. Ezek a PDF-fájlok a %VSINSTALLDIR%\SDK\v2.0\szimbólumokra vannak telepítve. Válassza az OK lehetőséget.

    Másodszor, a mscoree.dll és mscorwks.dll PDB-k letölthetők a Microsoft Symbol Serverről. A Szimbólumkiszolgáló konfigurálásához nyissa meg a szimbólumkeresési útvonal beállításai párbeszédpanelt. (Az Eszközök menüben válassza a Beállítások lehetőséget. A Beállítások párbeszédpanel bal oldali ablaktábláján nyissa meg a hibakeresési csomópontot, és válassza a Szimbólumok lehetőséget.) Adja hozzá ezt a keresési útvonalat a keresési listához: https://msdl.microsoft.com/download/symbols. Adjon hozzá egy szimbólumgyorsítótár-könyvtárat a szimbólumkiszolgáló gyorsítótár szövegmezőjéhez. Válassza az OK lehetőséget.

  2. A hibakereső mód beállítása natív módra.

    A megoldásban nyissa meg az indítási projekt Tulajdonságok rácsát. Válassza a Konfiguráció tulajdonságainak>hibakeresése lehetőséget. Állítsa a Hibakereső típusa tulajdonságot csak natívra.

  3. Indítsa el a hibakeresőt (F5).

  4. A diagnosztika létrehozásakor válassza az /clrÚjrapróbálkozás opciót, majd a Megszakítás-t.

  5. Nyissa meg a hívásverem ablakát. (A menüsávon válassza a Hibakeresés lehetőséget>Windows>Hívás verem.) A jogsértő DllMain vagy statikus inicializáló zöld nyíllal van azonosítva. Ha a jogsértő függvény nincs azonosítva, a következő lépéseket kell végrehajtani a kereséséhez.

  6. Nyissa meg az Azonnali ablakot (A menüsávon válassza aWindows>Azonnali> lehetőséget.)

  7. Írja be .load sos.dll az Azonnali ablakba az SOS hibakeresési szolgáltatás betöltéséhez.

  8. A belső !dumpstack verem teljes listájának beszerzéséhez írja be az /clr ablakba.

  9. Keresse meg a _CorDllMain (ha DllMain a problémát okozza) vagy _VTableBootstrapThunkInitHelperStub vagy GetTargetForVTableEntry első példányát (ha statikus inicializáló okozza a problémát). A hívás alatt található verembejegyzés az MSIL által implementált függvény meghívása, amely a rakodózár alatt próbálta végrehajtani a műveletet.

  10. Lépjen az előző lépésben azonosított forrásfájlra és sorszámra, és javítsa ki a problémát a Forgatókönyvek szakaszban leírt forgatókönyvek és megoldások használatával.

példa

Leírás

Az alábbi minta bemutatja, hogyan kerülheti el a betöltő zárolását úgy, hogy a kódot áthelyezi egy globális objektum konstruktorába.

Ebben a mintában van egy globális felügyelt objektum, amelynek konstruktora tartalmazza az eredetileg benne DllMainlévő felügyelt objektumot. A minta második része a szerelvényre hivatkozik, létrehozva a felügyelt objektum egy példányát, amely meghívja az inicializálást végrehajtó modulkonstruktort.

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

Ez a példa a vegyes szerelvények inicializálásával kapcsolatos problémákat mutatja be:

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

Ez a kód a következő kimenetet hozza létre:

Module ctor initializing based on global instance of class.

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

Lásd még

Vegyes (natív és kezelt) összeállítások