Megjegyzés
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhat bejelentkezni vagy módosítani a címtárat.
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhatja módosítani a címtárat.
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.cctorszerkezet 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 unmanagedirá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:
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.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.
Indítsa el a hibakeresőt (F5).
A diagnosztika létrehozásakor válassza az
/clrÚjrapróbálkozás opciót, majd a Megszakítás-t.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ő
DllMainvagy 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.Nyissa meg az Azonnali ablakot (A menüsávon válassza aWindows>Azonnali> lehetőséget.)
Írja be
.load sos.dllaz Azonnali ablakba az SOS hibakeresési szolgáltatás betöltéséhez.A belső
!dumpstackverem teljes listájának beszerzéséhez írja be az/clrablakba.Keresse meg a _CorDllMain (ha
DllMaina 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.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.