Jegyzet
Az oldalhoz való hozzáférés engedélyezést igényel. Próbálhatod be jelentkezni vagy könyvtárat váltani.
Az oldalhoz való hozzáférés engedélyezést igényel. Megpróbálhatod a könyvtár váltását.
Ez a cikk részletes javaslatokat tartalmaz konkrét nem biztonságos mintákra, az általuk járó kockázatokra és a kockázatok csökkentésére. Ezek az irányelvek azokat a fejlesztőket célják, akik nem biztonságos kódot írnak vagy tekintenek át C#-ban. Más .NET-nyelvek, például az F# és a Visual Basic nem tartoznak a jelen cikk hatókörébe, bár egyes javaslatok ezekre a nyelvekre is alkalmazhatók lehetnek.
Glossary
- AVE – Hozzáférés-megsértési kivétel.
- Byref – Felügyelt mutató (
ref T t), amely hasonló a nem felügyelt mutatóhoz, de a GC nyomon követi. Általában objektumok vagy a veremmemória tetszőleges pontjaira mutat. A referencia gyakorlatilag egy +0 eltolású kezelt mutató. - CVE – Nyilvánosan közzétett kiberbiztonsági biztonsági rések.
- JIT – Igény szerinti fordító (RyuJIT a CoreCLR-ben és a NativeAOT-ban).
- PGO – Profilvezérelt optimalizálás.
- Nem felügyelt mutató (vagy nyers mutató) – Olyan mutató (
T* p), amely tetszőleges memóriahelyre mutat, és amelyet a GC nem kezel vagy követ nyomon.
További kifejezésekért lásd a .NET futtatókörnyezet szószedetét.
Gyakori megbízhatatlan minták
A C# biztonságos környezetet biztosít, ahol a fejlesztőknek nem kell aggódniuk a futtatókörnyezet és a GC belső működése miatt. A nem biztonságos kód lehetővé teszi ezeknek a biztonsági ellenőrzéseknek a megkerülését, ami megbízhatatlan mintákat eredményezhet, amelyek memóriasérüléshez vezethetnek. Bár az ilyen minták bizonyos helyzetekben hasznosak lehetnek, körültekintően és csak akkor érdemes használni őket, ha feltétlenül szükséges. A C# és a .NET nem biztosít eszközöket a nem biztonságos kódok megfelelőségének ellenőrzéséhez (ahogy a különböző C/C++ fertőtlenítők is biztosíthatják), a GC-specifikus viselkedések további kockázatokat jelenthetnek a nem biztonságos C# esetében azon túl, amelyeket a hagyományos C/C++ fejlesztők ismerhetnek.
A felügyelt hivatkozásokra vonatkozó nem biztonságos kódot a következő konzervatív feltételezésekkel kell írni:
- A GC bármely metódus végrehajtását bármikor megszakíthatja bármely utasításnál.
- A GC áthelyezheti az objektumokat a memóriában, és frissítheti az összes követett hivatkozást.
- A GC pontosan tudja, hogy mikor már nincs szükség hivatkozásokra.
A halom sérülésének klasszikus példája akkor fordul elő, ha a GC elveszíti egy objektumhivatkozás nyomon követését, vagy az érvénytelen mutatókat halomhivatkozásként kezeli. Ez gyakran nem determinisztikus összeomlásokat vagy memóriasérülést eredményez. A halomsérülési hibák diagnosztizálása és reprodukálása különösen nehéz feladat, mert:
- Ezek a problémák hosszú ideig rejtve maradhatnak, és csak egy nem kapcsolódó kódmódosítás vagy futtatókörnyezeti frissítés után jelentkeznek.
- Ezek gyakran pontos időzítést igényelnek a reprodukálásához, például a GC megszakítja a végrehajtást egy adott helyen, és megkezdi a halom tömörítését, ami ritka és nemdeterminista esemény.
A következő szakaszok ismertetik a gyakori nem biztonságos mintákat, és ezekre vonatkozóan a ✔️ DO és ❌ DON'T ajánlásokat.
1. Nem követett kezelt mutatók (Unsafe.AsPointer és hasonló elemek)
A felügyelt (nyomon követett) mutatót nem lehet nem felügyelt (nem nyomon követett) mutatóvá alakítani a biztonságos C#-ban. Amikor ilyen igény felmerül, csábító lehet Unsafe.AsPointer<T>(T) használata fixed utasítás többletterhelésének elkerülése érdekében. Bár vannak érvényes használati esetek, fennáll annak a kockázata, hogy nem nyomon követett mutatókat hoz létre az áthelyezhető objektumokhoz.
Példa:
unsafe void UnreliableCode(ref int x)
{
int* nativePointer = (int*)Unsafe.AsPointer(ref x);
nativePointer[0] = 42;
}
Ha a csoportházirend-objektum megszakítja a metódus végrehajtását közvetlenül az UnreliableCode egérmutató beolvasása után (a hivatkozott cím x) és áthelyezi a hivatkozott objektumot, a csoportházirend-objektum megfelelően frissíti a tárolt x helyet, de nem tud semmit, nativePointer és nem frissíti a benne található értéket. Az írás nativePointer ekkor már tetszőleges memóriaterületekre történik.
unsafe void UnreliableCode(ref int x)
{
int* nativePointer = (int*)Unsafe.AsPointer(ref x);
// <-- GC happens here between the two lines of code and updates `x` to point to a new location.
// However, `nativePointer` still points to the old location as it's not reported to the GC
nativePointer[0] = 42; // Potentially corrupting write, access violation, or other issue.
}
Miután a GC folytatja a metódus végrehajtását, a rendszer a 42-et a régi helyre xírja, ami váratlan kivételhez, általános globális állapotsérüléshez vagy hozzáférés-megsértésen keresztüli folyamatleálláshoz vezethet.
Az ajánlott megoldás ehelyett az fixed operátor kulcsszójának és & címének használata annak biztosítására, hogy a csoportházirend-objektum ne helyezze át a célhivatkozást a művelet időtartamára.
unsafe void ReliableCode(ref int x)
{
fixed (int* nativePointer = &x) // `x` cannot be relocated for the duration of this block.
{
nativePointer[0] = 42;
}
}
Recommendations
-
❌ NE használjon
ref Xargumentumokat olyan implicit szerződéssel, amelyetXa GC mindig halmozott, rögzített vagy más módon nem helyez át. Ugyanez vonatkozik az egyszerű objektumokra és a spanokra is – ne vezessen be nem nyilvánvaló hívóalapú szerződéseket az élettartamukról a metódus-aláírásokban. Fontolja meg inkább egy refstruct argumentum használatát, vagy módosítsa az argumentumot nyers mutatótípusra (X*). - ❌ NE használjon mutatót Unsafe.AsPointer<T>(T) , ha ki tudja használni az eredeti objektumot, amelyre mutat. Az API dokumentációja szerint a hívónak kell garantálnia Unsafe.AsPointer<T>(T) , hogy a GC nem tudja áthelyezni a hivatkozást. Győződjön meg arról, hogy a kód véleményezői számára egyértelműen látható, hogy a hívó teljesítette ezt az előfeltételt.
- ✔️ HASZNÁLJON GCHandle-t vagy
fixedhatóköröket ahelyett, hogy Unsafe.AsPointer<T>(T)-t alkalmazna, hogy explicit hatóköröket hozzon létre a nem felügyelt mutatók számára, és biztosítsa, hogy az objektum mindig rögzítve legyen. - ✔️ Használjon nem felügyelt mutatókat (
fixed) a byrefs helyett, ha egy tömböt egy adott határhoz kell igazítania. Ez biztosítja, hogy a GC ne helyezze át az objektumot, és ne érvénytelenítse azokat az igazítási feltételezéseket, amelyekre a logika támaszkodhat.
2. Mutatók megjelenítése a fixed hatókörön kívül
Bár a rögzített kulcsszó definiálja a rögzített objektumból kapott mutató hatókörét, a mutató továbbra is elkerülheti a fixed hatókört, és hibákat eredményezhet, mivel a C# nem biztosít tulajdonosi/életciklus-védelmet.
Tipikus példa a következő kódrészlet:
unsafe int* GetPointerToArray(int[] array)
{
fixed (int* pArray = array)
{
_ptrField = pArray; // Bug!
Method(pArray); // Bug if `Method` allows `pArray` to escape,
// perhaps by assigning it to a field.
return pArray; // Bug!
// And other ways to escape the scope.
}
}
Ebben a példában a tömb megfelelően van rögzítve a fixed kulcsszóval (biztosítva, hogy a csoportházirend-objektum ne tudja áthelyezni azt a fixed blokkon belül), de a mutató a blokkon kívül fixed jelenik meg. Ez létrehoz egy lógó mutatót, amelynek hivatkozása nem definiált viselkedést eredményez.
Recommendations
- ✔️ Győződjön meg arról, hogy a
fixedblokkok mutatói nem hagyják el a megadott hatókört. - ✔️ A DO a biztonságos, alacsony szintű primitíveket részesíti előnyben beépített menekülési elemzéssel, például a C# ref szerkezetével. További információ: Alacsony szintű szerkezetfejlesztések.
3. A futtatókörnyezet és a kódtárak belső implementációjának részletei
Bár a belső megvalósítás részleteinek elérése vagy támaszkodása általában rossz gyakorlat (és a .NET nem támogatja), érdemes konkrét, gyakran megfigyelt eseteket felhívni. Ez nem a teljes lista az összes lehetséges dologról, amely hibás lehet, ha a kód helytelenül támaszkodik a belső implementáció részleteire.
Recommendations
❌ NE módosítsa vagy olvassa el az objektum fejlécének egy részét sem.
- Az objektumfejlécek a futtatókörnyezetekben eltérhetnek.
- A CoreCLR-ben az objektumfejléc nem érhető el biztonságosan az objektum rögzítése nélkül.
- A MethodTable mutató módosításával soha ne módosítsa az objektum típusát.
❌ Ne tároljon adatokat egy objektum kitöltő részében. Ne feltételezze, hogy a kitöltés tartalma megmarad, vagy a kitöltés alapértelmezés szerint mindig nullázva lesz.
❌ NE tegyen feltételezéseket a primitíveken és a szerkezeteken kívül más méretekkel és eltolásokkal kapcsolatban szekvenciális vagy explicit elrendezéssel. Még ekkor is vannak kivételek, például amikor GC-kezelésekről van szó.
❌ NE hívjon meg nem nyilvános metódusokat, ne férjen hozzá nem nyilvános mezőkhöz, és ne módosítson írásvédett mezőket a BCL típusokban reflektálással vagy nem biztonságos kóddal.
❌ NE feltételezze, hogy a BCL-ben egy adott nem nyilvános tag mindig megtalálható lesz, vagy meghatározott struktúrával fog rendelkezni. A .NET-csapat időnként módosítja vagy eltávolítja a nem nyilvános API-kat a karbantartási kiadásokban.
❌ NE módosítsa
static readonlya mezőket tükrözés vagy nem biztonságos kód használatával, mivel feltételezzük, hogy állandók. A RyuJIT például általában explicit állandóként illeszti be őket.❌ NE feltételezze egyszerűen, hogy egy hivatkozás nem helyezhető át. Ez az útmutató sztringekre és UTF-8 (
"..."u8) literálokra, statikus mezőkre, RVA-mezőkre, LOH-objektumokra stb. vonatkozik.- Ezek olyan futtatókörnyezeti megvalósítási részletek, amelyek egyes futtatókörnyezeti verziók esetében érvényesek lehetnek, másoknál azonban nem.
- Előfordulhat, hogy az ilyen objektumok nem felügyelt mutatói nem állítják le a szerelvények kiürítését, ami miatt a mutatók elcsúsznak. A helyesség biztosításához használjon
fixedhatóköröket.
ReadOnlySpan<int> rva = [1, 2, 4, 4]; int* p = (int*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(rva)); // Bug! The assembly containing the RVA field might be unloaded at this point // and `p` becomes a dangling pointer. int value = p[0]; // Access violation or other issue.❌ NE írjon olyan kódot, amely egy adott futtatókörnyezet implementálási részleteire támaszkodik.
4. Érvénytelen felügyelt mutatók (még akkor is, ha soha nem halasztják el őket)
A kód bizonyos kategóriái végül a mutató manipulálására és aritmetikai műveletekre támaszkodnak, és az ilyen kódok gyakran választhatnak a nem felügyelt mutatók () és a felügyelt mutatók (T* pref T p) használata között.
Ezek a mutatók tetszőlegesen módosíthatók, például a nem felügyelt mutatók operátorai (p++) és a felügyelt mutatók metódusai (Unsafe) segítségévelp = ref Unsafe.Add(ref p, 1). Mindkettő "nem biztonságos kód", és mindkettővel megbízhatatlan minták hozhatók létre. Bizonyos algoritmusok esetében azonban egyszerűbb lehet véletlenül GC-nem biztonságos mintákat létrehozni a felügyelt mutatók manipulálásakor. Mivel a nem felügyelt mutatókat a GC nem követi nyomon, az általuk tartalmazott érték csak akkor releváns, ha a fejlesztő kódja elhalasztja. Ezzel szemben a felügyelt mutató értéke nem csak akkor releváns, ha a fejlesztő kódja kiválasztja az értékét, hanem akkor is, ha a GC megvizsgálja. Így a fejlesztő következmények nélkül hozhat létre érvénytelen nem felügyelt mutatókat, feltéve, hogy nem hivatkozzák meg őket, de az érvénytelen felügyelt mutató létrehozása hiba. Példa:
unsafe void UnmanagedPointers(int[] array)
{
fixed (int* p = array)
{
int* invalidPtr = p - 1000;
// invalidPtr is pointing to an undefined location in memory
// it's ok as long as it's not dereferenced.
int* validPtr = invalidPtr + 1000; // Returning back to the original location
*validPtr = 42; // OK
}
}
A byrefst (felügyelt mutatókat) használó hasonló kód azonban érvénytelen.
void ManagedPointers_Incorrect(int[] array)
{
ref int invalidPtr = ref Unsafe.Add(ref array[0], -1000); // Already a bug!
ref int validPtr = ref Unsafe.Add(ref invalidPtr, 1000);
validPtr = 42; // possibly corrupting write
}
Bár a felügyelt implementáció itt elkerüli a kisebb rögzítési többletterhelést, ez nem megoldás, mert invalidPtr külső mutatóvá válhat, miközben a GC frissíti a tényleges címétarray[0].
Az ilyen hibák rejtettek, és még a .NET is problémákba ütközött velük a fejlesztés során.
Recommendations
-
❌ NE hozzon létre érvénytelen felügyelt mutatókat, még akkor sem, ha azokat nem hivatkozzák, vagy olyan kódútvonalakon találhatók, amelyeket soha nem hajtanak végre.
- További információ az érvényes felügyelt mutatóról: ECMA-335, Sec. II.14.4.2 Felügyelt mutatók; és ECMA-335 CLI specifikációs kiegészítés, II.14.4.2.
- ✔️ NE használjon rögzített, nem felügyelt mutatókat, ha az algoritmus ilyen manipulációkat igényel.
5. Újraértelmezés-szerű típuskonverziók
Bár minden típusú struct-to-class vagy class-to-struct kasztolás definíció szerint meghatározatlan viselkedés, előfordulhatnak megbízhatatlan minták struct-to-struct vagy class-to-class átalakítások során is. A megbízhatatlan minták tipikus példája a következő kód:
struct S1
{
string a;
nint b;
}
struct S2
{
string a;
string b;
}
S1 s1 = ...
S2 s2 = Unsafe.As<S1, S2>(ref s1); // Bug! A random nint value becomes a reference reported to the GC.
És még ha az elrendezés is hasonló, akkor is óvatosnak kell lennie, ha GC-hivatkozások (mezők) szerepelnek benne.
Recommendations
- ❌ NE alakítsa át a struktúrákat osztályokká, vagy osztályokat struktúrákká.
-
❌ NE használja
Unsafe.Asa struct-to-struct vagy class-to-class átalakítást, kivéve, ha teljesen biztos abban, hogy a konvertálás legális. További információkért tekintse meg az API-dokumentumok Megjegyzések szakaszátUnsafe.As. - ✔️ A DO a biztonságosabb mezőenkénti másolást, a külső kódtárakat, például az AutoMappert vagy a Forrásgenerátorokat részesíti előnyben az ilyen átalakításokhoz.
- ✔️ Használja inkább a
Unsafe.BitCast-t aUnsafe.As-t helyett, mert aBitCastnéhány alapszintű használati ellenőrzést nyújt. Vegye figyelembe, hogy ezek az ellenőrzések nem biztosítanak teljes körű helyességi garanciát, ami azt jelenti, hogyBitCasttovábbra is nem biztonságos API-nak minősülnek.
6. Az írási akadály megkerülése és a GC-hivatkozásokon végzett nem atomi műveletek
Általában a GC-hivatkozások mindenféle írása vagy olvasása mindig atomi. Emellett minden kísérlet arra, hogy egy GC-referenciát vagy egy GC-mezőket tartalmazó struktúrát byref módon hozzárendeljünk egy potenciális halomhelyhez, átmegy az írási akadály mechanizmusán, amely biztosítja, hogy a GC értesüljön az objektumok közötti új kapcsolatokról. A nem biztonságos kód azonban lehetővé teszi számunkra, hogy megkerüljük ezeket a garanciákat, és megbízhatatlan mintákat vezethessünk be. Példa:
unsafe void InvalidCode1(object[] arr1, object[] arr2)
{
fixed (object* p1 = arr1)
fixed (object* p2 = arr2)
{
nint* ptr1 = (nint*)p1;
nint* ptr2 = (nint*)p2;
// Bug! We're assigning a GC pointer to a heap location
// without going through the Write Barrier.
// Moreover, we also bypass array covariance checks.
*ptr1 = *ptr2;
}
}
Hasonlóképpen, a felügyelt mutatókkal rendelkező következő kód is megbízhatatlan:
struct StructWithGcFields
{
object a;
int b;
}
void InvalidCode2(ref StructWithGcFields dst, ref StructWithGcFields src)
{
// It's already a bad idea to cast a struct with GC fields to `ref byte`, etc.
ref byte dstBytes = ref Unsafe.As<StructWithGcFields, byte>(ref dst);
ref byte srcBytes = ref Unsafe.As<StructWithGcFields, byte>(ref src);
// Bug! Bypasses the Write Barrier. Also, non-atomic writes/reads for GC references.
Unsafe.CopyBlockUnaligned(
ref dstBytes, ref srcBytes, (uint)Unsafe.SizeOf<StructWithGcFields>());
// Bug! Same as above.
Vector128.LoadUnsafe(ref srcBytes).StoreUnsafe(ref dstBytes);
}
Recommendations
- ❌ NE használjon nem atomi műveleteket a GC-hivatkozásokon (például a SIMD-műveletek gyakran nem adják meg őket).
- ❌ Ne használjon nem felügyelt mutatókat a GC-hivatkozások halomhelyre való tárolásához (az írási akadály kihagyásával).
7. Feltételezések az objektum élettartamáról (véglegesítők, GC.KeepAlive)
Kerülje az objektumok élettartamával kapcsolatos feltételezéseket a GC szemszögéből. Pontosabban, ne feltételezzük, hogy egy objektum még életben van, ha lehet, hogy nem. Az objektumélettartamok különböző futtatókörnyezetekben vagy akár ugyanazon metódus különböző szintjei között is változhatnak (a RyuJIT 0. és 1. rétege). Az olyan finalizálók gyakori példák lehetnek, ahol az ilyen feltételezések helytelenek lehetnek.
public class MyClassWithBadCode
{
public IntPtr _handle;
public void DoWork() => DoSomeWork(_handle); // A use-after-free bug!
~MyClassWithBadCode() => DestroyHandle(_handle);
}
// Example usage:
var obj = new MyClassWithBadCode()
obj.DoWork();
Ebben a példában a DestroyHandle meghívható, mielőtt a DoWork befejeződne, vagy akár megkezdődne.
Ezért kulcsfontosságú, hogy ne feltételezzük, hogy az objektumok, például thisa metódus végéig életben maradnak.
void DoWork()
{
// A pseudo-code of what might happen under the hood:
IntPtr reg = this._handle;
// 'this' object is no longer alive at this point.
// <-- GC interrupts here, collects the 'this' object, and triggers its finalizer.
// DestroyHandle(_handle) is called.
// Bug! 'reg' is now a dangling pointer.
DoSomeWork(reg);
// You can resolve the issue and force 'this' to be kept alive (thus ensuring the
// finalizer will not run) by uncommenting the line below:
// GC.KeepAlive(this);
}
Ezért javasoljuk, hogy explicit módon meghosszabbítsa az objektumok élettartamát a használatával GC.KeepAlive(Object) vagy SafeHandle.
A probléma egy másik klasszikus példánya az Marshal.GetFunctionPointerForDelegate<TDelegate>(TDelegate) API:
var callback = new NativeCallback(OnCallback);
// Convert delegate to function pointer
IntPtr fnPtr = Marshal.GetFunctionPointerForDelegate(callback);
// Bug! The delegate might be collected by the GC here.
// It should be kept alive until the native code is done with it.
RegisterCallback(fnPtr);
Recommendations
-
❌ NE tegyen feltételezéseket az objektumok élettartamáról. Például soha ne feltételezzük, hogy
thismindig él a módszer végéig. - Használja a SafeHandle a natív erőforrások kezeléséhez.
- ✔️ A DO használatával GC.KeepAlive(Object) szükség esetén meghosszabbíthatja az objektumok élettartamát.
Szálközi hozzáférés a helyi változókhoz
A helyi változók elérése egy másik szálról általában rossz gyakorlatnak számít. A .NET memóriamodellben ismertetett módon azonban explicit módon nem definiált viselkedést vált ki, ha felügyelt hivatkozásokról van szó.
Példa: Előfordulhat, hogy a GC-hivatkozásokat tartalmazó szerkezetek nem szálbiztos módon nullázhatók vagy felülírhatók egy no-GC régióban, miközben egy másik szál olvassa azt, ami meghatározatlan viselkedéshez vezet.
Recommendations
- ❌ NE érje el a helyieket a szálak között (különösen ha GC-hivatkozásokat tartalmaznak).
- ✔️ NE használjon halom vagy nem felügyelt memóriát (például NativeMemory.Alloc) helyette.
9. Nem biztonságos korlátellenőrzés eltávolítása
A C#-ban az összes idiomatikus memóriahozzáférés alapértelmezés szerint korlátokat tartalmaz. A JIT-fordító eltávolíthatja ezeket az ellenőrzéseket, ha bizonyítani tudja, hogy szükségtelenek, ahogy az alábbi példában is látható.
int SumAllElements(int[] array)
{
int sum = 0;
for (int i = 0; i < array.Length; i++)
{
// The JIT knows that within this loop body, i >= 0 and i < array.Length.
// The JIT can reason that its own bounds check would be duplicative and
// unnecessary, so it opts not to emit the bounds check into the final
// generated code.
sum += array[i];
}
}
Bár a JIT folyamatosan fejleszti az ilyen minták felismerését, még mindig vannak olyan forgatókönyvek, amelyekben az ellenőrzések érvényben maradnak, ami hatással lehet a gyakori kódot érintő teljesítményre. Ilyen esetekben előfordulhat, hogy nem biztonságos kód használatával manuálisan távolítja el ezeket az ellenőrzéseket anélkül, hogy teljesen tisztában lenne a kockázatokkal, vagy pontosan értékelné a teljesítmény előnyeit.
Vegyük például az alábbi módszert.
int FetchAnElement(int[] array, int index)
{
return array[index];
}
Ha a JIT nem tudja bizonyítani, hogy index mindig jogilag a array határain belül van, akkor átírja a módszert, hogy így nézzen ki, mint az alábbi.
int FetchAnElement_AsJitted(int[] array, int index)
{
if (index < 0 || index >= array.Length)
throw new IndexOutOfBoundsException();
return array.GetElementAt(index);
}
A gyakran futó kódot tartalmazó ellenőrzés többletterhelésének csökkentése érdekében késztetést érezhet, hogy nem biztonságos egyenértékű API-kat (Unsafe és MemoryMarshal) használjon.
int FetchAnElement_Unsafe1(int[] array, int index)
{
// DANGER: The access below is not bounds-checked and could cause an access violation.
return Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(array), index);
}
Vagy használj rögzítést és nyers mutatókat:
unsafe int FetchAnElement_Unsafe2(int[] array, int index)
{
fixed (int* pArray = array)
{
// DANGER: The access below is not bounds-checked and could cause an access violation.
return pArray[index];
}
}
Ez véletlenszerű összeomlásokhoz vagy állapotsérüléshez vezethet, ha index kívül esik a tartományon array.
Az ilyen nem biztonságos átalakítások teljesítménybeli előnyökkel járhatnak a nagyon gyakori útvonalakon, de ezek az előnyök gyakran átmenetiek, mivel minden .NET-kiadás javítja a JIT azon képességét, hogy megszüntesse a szükségtelen korlátok ellenőrzését, ha biztonságos.
Recommendations
- ✔️ ELLENŐRIZZE, hogy a .NET legújabb verziója továbbra sem tudja-e megszüntetni a korlátok ellenőrzését. Ha lehet, írja át biztonságos kóddal. Ellenkező esetben jelentsen egy problémát a RyuJIT-nek. Ezt a nyomon követési problémát érdemes kiindulópontként használni.
- ✔️ A DO a valós teljesítményre gyakorolt hatást méri. Ha a teljesítménynövekedés elhanyagolható, vagy a kód nem bizonyult túl gyakorinak egy aprólékos mikrobenchjelen kívül, írja át biztonságos kóddal.
- ✔️ A DO további tippeket ad a JIT-nek, például a ciklusok előtti manuális határellenőrzéseket és a mezők helyieknek való mentését, mivel a .NET memóriamodell bizonyos esetekben megakadályozhatja, hogy a JIT eltávolítsa a határellenőrzéseket.
- ✔️ Ellenőrizze a kódot határellenőrzésekkel, ha továbbra is szükség van nem biztonságos kódra. Tekintse meg az alábbi példát.
Debug.Assert(array is not null);
Debug.Assert((index >= 0) && (index < array.Length));
// Unsafe code here
Ezeket az ellenőrzéseket újrafelhasználható segédmetódusokká is átalakíthatja.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static T UnsafeGetElementAt<T>(this T[] array, int index)
{
Debug.Assert(array is not null);
Debug.Assert((index >= 0) && (index < array.Length));
return Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(array), index);
}
A Debug.Assert belefoglalás nem biztosít a kiadási buildek megfelelőségének ellenőrzését, de segíthet észlelni a hibakeresési buildek lehetséges hibáit.
10. Memóriahozzáférés egyesítése
Lehet, hogy csábítást érez arra, hogy nem biztonságos kódot használjon a memóriahozzáférések egyesítésére a teljesítmény javítása érdekében.
A klasszikus példa a következő kód, amely egy karaktertömbbe írja "False":
// Naive implementation
static void WriteToDestination_Safe(char[] dst)
{
if (dst.Length < 5) { throw new ArgumentException(); }
dst[0] = 'F';
dst[1] = 'a';
dst[2] = 'l';
dst[3] = 's';
dst[4] = 'e';
}
// Unsafe coalesced implementation
static void WriteToDestination_Unsafe(char[] destination)
{
Span<char> dstSpan = destination;
if (dstSpan.Length < 5) { throw new ArgumentException(); }
ulong fals_val = BitConverter.IsLittleEndian ? 0x0073006C00610046ul : 0x00460061006C0073ul;
MemoryMarshal.Write(MemoryMarshal.AsBytes(dstSpan.Slice(0, 4)), in fals_val); // Write "Fals" (4 chars)
dstSpan[4] = 'e'; // Write "e" (1 char)
}
A .NET korábbi verzióiban a nem biztonságos verzió használata MemoryMarshal mérhetően gyorsabb volt, mint az egyszerű biztonságos verzió. A .NET modern verziói azonban sokkal továbbfejlesztett JIT-t tartalmaznak, amely mindkét esetben egyenértékű kodeket állít elő. A .NET 10-ből az x64-kodek a következő:
; WriteToDestination_Safe
cmp eax, 5
jl THROW_NEW_ARGUMENTEXCEPTION
mov rax, 0x73006C00610046
mov qword ptr [rdi+0x10], rax
mov word ptr [rdi+0x18], 101
; WriteToDestination_Unsafe
cmp edi, 5
jl THROW_NEW_ARGUMENTEXCEPTION
mov rdi, 0x73006C00610046
mov qword ptr [rax], rdi
mov word ptr [rax+0x08], 101
A kódnak még egyszerűbb és olvashatóbb verziója van:
"False".CopyTo(dst);
A .NET 10-hez hasonlóan ez a hívás a fenti kodekeket hozza létre. Még egy további előnye is van: azt jelzi a JIT-nek, hogy a szigorú elemenkénti írásoknak nem kell atominak lenniük. A JIT kombinálhatja ezt a tippet más környezeti ismeretekkel, hogy még több optimalizálást biztosítson az itt tárgyaltakon túl.
Recommendations
- Előnyben részesítsük az idiomatikus biztonságos kódot a nem biztonságos kóddal szemben a memóriahozzáférés összevonása során.
- Adatok másolásához használja előnyben
Span<T>.CopyTo-t ésSpan<T>.TryCopyTo-t. - Előnyben részesítse a
String.Equalsés aSpan<T>.SequenceEqualaz adatok összehasonlítására (akár akkor is, ha aStringComparer.OrdinalIgnoreCase-t használja). - Inkább
Span<T>.Fillaz adatok kitöltéséhez ésSpan<T>.Clearaz adatok törléséhez. - Vegye figyelembe, hogy az elemenkénti vagy a mezőnkénti írásokat/olvasásokat a JIT automatikusan összevonhatja.
- Adatok másolásához használja előnyben
- ✔️ Ha idiomatikus kódot ír, és észleli, hogy a dotnet/runtime nem optimális teljesítményt nyújt, jelentsen be hibát.
- ❌ NE egyesítse manuálisan a memóriahozzáféréseket, ha nem biztos a nem igazodó memóriahozzáférési kockázatokban, az atomi garanciákban vagy a kapcsolódó teljesítményelőnyökben.
11. Nem igazított memóriahozzáférés
A memóriahozzáférési egybeolvasztásban leírt műveletek gyakran explicit vagy implicit nem igazodó olvasásokat/írásokat eredményeznek. Bár ez általában nem okoz komoly problémákat (eltekintve a gyorsítótár és az oldalhatárok átlépése miatti lehetséges teljesítménybírságoktól), még mindig komoly kockázatokat jelent.
Vegyük például azt a forgatókönyvet, amikor egyszerre töröl egy tömb két elemét:
uint[] arr = _arr;
arr[i + 0] = 0;
arr[i + 1] = 0;
Tegyük fel, hogy ezeken a helyeken a korábbi értékek is uint.MaxValue (0xFFFFFFFF) voltak.
A .NET memóriamodell garantálja, hogy mindkét művelet atomi jellegű, így a folyamat összes többi szála csak az új értéket 0, vagy a régi értéket 0xFFFFFFFF fogja megfigyelni, soha nem "széttöredezett" értékeket, például 0xFFFF0000.
Tegyük fel azonban, hogy a rendszer a következő nem biztonságos kódot használja a korlátok ellenőrzésének megkerülésére, és mindkét elemet egyetlen 64 bites tárolóval nullázhatja:
ref uint p = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(arr), i);
Unsafe.WriteUnaligned<ulong>(ref Unsafe.As<uint, byte>(ref p), 0UL);
Ez a kód az atomitási garancia eltávolításának mellékhatása. A szakadt értékeket más szálak is megfigyelhetik, ami nem definiált viselkedéshez vezet. Ahhoz, hogy egy ilyen szenes írás atomi legyen, a memóriát az írás méretéhez kell igazítani (ebben az esetben 8 bájt). Ha a művelet előtt megpróbálja manuálisan igazítani a memóriát, figyelembe kell vennie, hogy a GC bármikor áthelyezheti (és hatékonyan módosíthatja a tömb igazítását), ha nincs rögzítve. További részletekért tekintse meg a .NET memóriamodell dokumentációját.
A nem elaltatott memóriahozzáférés másik kockázata, hogy bizonyos esetekben az alkalmazás összeomlik. Míg egyes .NET-futtatókörnyezetek az operációs rendszerre támaszkodnak a rosszul igazított hozzáférések javításához, bizonyos platformokon még mindig vannak olyan forgatókönyvek, amelyekben a rosszul igazított hozzáférés DataMisalignedException-t vagy SEHException-t okozhat. Néhány példa:
-
Interlockedegyes platformokon helytelenül kiajánlott memórián végzett műveletek (például). - Helytelenül értelmezett lebegőpontos műveletek az ARM-en.
- Speciális eszközmemória elérése bizonyos igazítási követelményekkel (a .NET nem igazán támogatja).
Recommendations
- ❌ NE használjon korlátlan memóriahozzáféréseket zárolásmentes algoritmusokban és más olyan forgatókönyvekben, ahol fontos az atomiság.
- ✔️ HA szükséges, manuálisan igazítsa az adatokat, de ne feledje, hogy a GC bármikor áthelyezheti az objektumokat, és tulajdonképpen dinamikusan módosíthatja az igazítást. Ez különösen fontos a SIMD különböző
StoreAligned/LoadAlignedAPI-k esetében. - ✔️ HASZNÁLJ explicit, igazítatlan olvasási/írási API-kat, mint például Unsafe.ReadUnaligned/Unsafe.WriteUnaligned, az igazított API-k, mint például Unsafe.Read<T>(Void*)/Unsafe.Write<T>(Void*, T) vagy Unsafe.As<TFrom,TTo>(TFrom) helyett, ha az adatok nincsenek megfelelően igazítva.
- ✔️ Ne feledje, hogy a különböző memóriamanipulációs API-k, például Span<T>.CopyTo(Span<T>) nem nyújtanak atomisági garanciát.
- ✔️ Kérjük, tekintse meg a .NET memóriamodelljének dokumentációját (lásd a hivatkozásokat) a részletekért az atomitási garanciákról.
- ✔️ DO mérje a teljesítményt az összes célplatformon, mivel egyes platformok jelentős teljesítménybírságot rónak ki a nem igazított memóriahozzáférésekre. Előfordulhat, hogy ezeken a platformokon a naiv kód jobban teljesít, mint az okos kód.
- ✔️ Ne feledje, hogy vannak olyan forgatókönyvek és platformok, ahol a nem igazított memóriahozzáférés kivételhez vezethet.
12. Structok bináris (de)szerializálása kitöltésekkel vagy nem blittelhető tagokkal
Legyen óvatos, ha különböző szerializációs jellegű API-kat alkalmaz struktúrák bájttömbökbe vagy azokból történő másolásához vagy olvasásához.
Ha egy szerkezet kitöltéseket vagy nem blittelhető tagokat (például bool vagy GC-mezőket) tartalmaz, akkor a klasszikus nem biztonságos memóriaműveletek, mint például Fill, CopyTo és SequenceEqual, véletlenül átmásolhatják a bizalmas adatokat a veremből a kitöltésekbe, vagy összehasonlítás közben a szemétadatokat jelentősnek értékelhetik, ami ritkán reprodukálható hibákat okoz. A gyakori antipattern a következőképpen nézhet ki:
T UnreliableDeserialization<TObject>(ReadOnlySpan<byte> data) where TObject : unmanaged
{
return MemoryMarshal.Read<TObject>(data); // or Unsafe.ReadUnaligned
// BUG! TObject : unmanaged doesn't guarantee that TObject is blittable and contains no paddings.
}
Az egyetlen helyes módszer a mezők szerinti terhelések/tárolók használata minden bemenethez TObject (vagy általánosítva a Reflection, a Source Generators vagy a (de)szerializációs kódtárak esetében).
Recommendations
-
❌ NE használjon nem biztonságos kódot a struktúrák másolásához/betöltéséhez/összehasonlításához kitöltésekkel vagy nem blittelhető tagokkal. A nem megbízható bemenetekből származó terhelések még az olyan alaptípusok esetében is problémásak, mint a
bool.decimalUgyanakkor előfordulhat, hogy a tárolók véletlenül szerializálják a stack bizalmas információit a struktúrák hézagaiban/paddingjaiban. -
❌ NE támaszkodjon kényszerre
T : unmanagedvagy hasonló API-ra annak garantálása érdekében,RuntimeHelpers.IsReferenceOrContainsReferenceshogy egy általános típus biztonságosan végrehajtsa a bitenkénti műveleteket. Ezen irányelvek írásakor nincs megbízható programozott módszer annak meghatározására, hogy egy adott típuson megengedett-e tetszőleges bitenkénti műveletek végrehajtása.- Ha ilyen bitenkénti manipulációt kell végrehajtania, azt csak ezen a rögzített típuslistán végezze el, és legyen tisztában az aktuális gép endianitásával:
- A primitív integráltípusok
Byte,SByte,Int16,UInt16,Int32,UInt32ésInt64UInt64; - Egy
Enumaz egyik fenti primitív integráltípus által támogatott; -
Char,Int128,UInt128,Half,Single,Double,IntPtr,UIntPtr.
- A primitív integráltípusok
- Ha ilyen bitenkénti manipulációt kell végrehajtania, azt csak ezen a rögzített típuslistán végezze el, és legyen tisztában az aktuális gép endianitásával:
- ✔️ Mezőnkénti betöltést/mentést használjon (de)szerializálás helyett. Fontolja meg a népszerű és biztonságos kódtárak használatát a (de)szerializáláshoz.
13. Null értékű felügyelt mutató
Byrefs (felügyelt mutatók) általában ritkán nulla értékűek, és a null byrefek létrehozásának egyetlen biztonságos módja jelenleg az, ha egy ref struct-t inicializálunk a default értékkel. Ezután az összes ref mezője null értékű felügyelt mutató:
RefStructWithRefField s = default;
ref byte nullRef = ref s.refFld;
A null byrefek létrehozására azonban számos nem biztonságos módszer létezik. Néhány példa:
// Null byref by calling Unsafe.NullRef directly:
ref object obj = ref Unsafe.NullRef<object>();
// Null byref by turning a null unmanaged pointer into a null managed pointer:
ref object obj = ref Unsafe.AsRef<object>((void*)0);
A memóriabiztonsággal kapcsolatos problémák bevezetésének kockázata alacsony, és bármilyen kísérlet egy null byref dereferálására egy jól definiált NullReferenceException-hez vezet. A C#-fordító azonban feltételezi , hogy a byref elhalasztása mindig sikeres, és nem okoz megfigyelhető mellékhatást. Ezért legális optimalizálás minden olyan hivatkozás megszüntetésére, amelynek eredményét azonnal el kell vetni. Lásd: dotnet/runtime#98681 (és ez a kapcsolódó megjegyzés) egy olyan ,NET-en belüli, most már kijavított hiba példájáért, amelyben a kódtár kódja helytelenül támaszkodott a mellékhatást kiváltó dereferensre, és nem tudja, hogy a C#-fordító ténylegesen rövidre kapcsolta a kívánt logikát.
Recommendations
- ❌ Amennyiben nincs rá szükség, ne hozzon létre null byref hivatkozást a C#-ban. Érdemes lehet inkább normál felügyelt hivatkozásokat, null objektummintát vagy üres spanokat használni.
- ❌ NE dobja el a byref dereferencia eredményét, mivel ez optimalizálva lehet, és potenciális hibákhoz vezethet.
14. stackalloc
stackalloc korábban kis méretű, nem menekülő tömbök létrehozására használták a veremen, csökkentve a GC-nyomást. A jövőben a JIT Escape Analysisje elkezdheti optimalizálni a tömbök nem menekülő GC-foglalásait az objektumok halmozására, ami redundánssá válhat stackalloc . A stackalloc addig is hasznos marad a veremen kis pufferek kiosztásához. Nagyobb vagy menekülő pufferek esetén gyakran kombinálják .ArrayPool<T>
Recommendations
✔️ A DO mindig a kifejezés bal oldalán használja
stackallocReadOnlySpan<T>/Span<T>a korlátok ellenőrzéséhez:// Good: Span<int> s = stackalloc int[10]; s[2] = 0; // Bounds check is eliminated by JIT for this write. s[42] = 0; // IndexOutOfRangeException is thrown // Bad: int* s = stackalloc int[10]; s[2] = 0; s[42] = 0; // Out of bounds write, undefined behavior.❌ NE használjon
stackallocbelső hurkokat. A veremterület nem lesz visszanyerve, amíg a metódus vissza nem tér, így egystackallochurokban való használata a folyamat megszüntetését eredményezheti a verem túlcsordulása miatt.❌ NE használjon nagy hosszokat a
stackallocesetében. Például 1024 bájt ésszerű felső határnak tekinthető.✔️ Ellenőrizze a változók tartományát, amelyeket
stackallochosszúságként használnak.void ProblematicCode(int length) { Span<int> s = stackalloc int[length]; // Bad practice: check the range of `length`! Consume(s); }Rögzített verzió:
void BetterCode(int length) { // The "throw if length < 0" check below is important, as attempting to stackalloc a negative // length will result in process termination. ArgumentOutOfRangeException.ThrowIfLessThan(length, 0, nameof(length)); Span<int> s = length <= 256 ? stackalloc int[length] : new int[length]; // Or: // Span<int> s = length <= 256 ? stackalloc int[256] : new int[length]; // Which performs a faster zeroing of the stackalloc, but potentially consumes more stack space. Consume(s); }✔️ A DO modern C#-funkciókat, például gyűjteménykonstansokat (
Span<int> s = [1, 2, 3];)params Span<T>és beágyazott tömböket használ a manuális memóriakezelés elkerülése érdekében, ha lehetséges.
15. Rögzített méretű pufferek
A rögzített méretű pufferek más nyelvekből vagy platformokról származó adatforrásokkal való együttműködéshez hasznosak voltak. Ezután biztonságosabb és kényelmesebb beágyazott tömbök váltották fel őket.
Egy rögzített méretű pufferre példa, amelyhez unsafe környezet szükséges, a következő kódrészlet:
public struct MyStruct
{
public unsafe fixed byte data[8];
// Some other fields
}
MyStruct m = new();
ms.data[10] = 0; // Out-of-bounds write, undefined behavior.
A modern és biztonságosabb alternatíva a beágyazott tömbök:
[System.Runtime.CompilerServices.InlineArray(8)]
public struct Buffer
{
private int _element0; // can be generic
}
public struct MyStruct
{
public Buffer buffer;
// Some other fields
}
MyStruct ms = new();
ms.buffer[i] = 0; // Runtime performs a bounds check on index 'i'; could throw IndexOutOfRangeException.
ms.buffer[7] = 0; // Bounds check elided; index is known to be in range.
ms.buffer[10] = 0; // Compiler knows this is out of range and produces compiler error CS9166.
A rögzített méretű pufferek kerülésének másik oka, hogy az alapértelmezés szerint mindig nullára inicializált beágyazott tömbök előnyösebbek, mivel a rögzített méretű pufferek tartalma bizonyos esetekben nem feltétlenül nulla.
Recommendations
- ✔️ Lehetőleg cserélje le a fix méretű puffereket beágyazott tömbökre vagy IL marshaller attribútumokra.
16. Egybefüggő adatok mutatók és hosszúságok segítségével történő továbbítása (vagy nullával való lezárásra támaszkodva)
Kerülje az olyan API-k definiálását, amelyek nem felügyelt vagy felügyelt mutatókat fogadnak az egybefüggő adatokhoz. Ehelyett használja a következőt Span<T>ReadOnlySpan<T>:
// Poor API designs:
void Consume(ref byte data, int length);
void Consume(byte* data, int length);
void Consume(byte* data); // zero-terminated
void Consume(ref byte data); // zero-terminated
// Better API designs:
void Consume(Span<byte> data);
void Consume(Memory<byte> data);
void Consume(byte[] data);
void Consume(byte[] data, int offset, int length);
A zéró leállítás különösen kockázatos, mert nem minden puffer zéró megszakítású, és a nulla terminátoron túli olvasás információfeltáráshoz, adatsérüléshez vagy folyamatmegszüntetéshez vezethet hozzáférés-megsértés miatt.
Recommendations
❌ NE tegye közzé azokat a metódusokat, amelyek argumentumai mutatótípusok (nem felügyelt mutatók
T*vagy felügyelt mutatókref T), ha ezek az argumentumok puffereket jelölnek. Használjon inkább biztonságos puffertípusokatSpan<T>ReadOnlySpan<T>.❌ NE használjon implicit szerződéseket byref argumentumokhoz, például olyan helyzeteket, ahol minden hívónak a veremben kell lefoglalnia a bemenetet. Ha ilyen szerződésre van szükség, fontolja meg inkább a refstruct használatát.
❌ NE feltételezze, hogy a pufferek nullával végződnek, hacsak a forgatókönyv nem dokumentálja, hogy ez érvényes feltételezés. Annak ellenére, hogy a .NET garantálja, hogy
stringpéldányok és"..."u8literálok nullával végződnek, ugyanez nem igaz más puffertípusokra, mint példáulReadOnlySpan<char>vagychar[].unsafe void NullTerminationExamples(string str, ReadOnlySpan<char> span, char[] array) { Debug.Assert(str is not null); Debug.Assert(array is not null); fixed (char* pStr = str) { // OK: Strings are always guaranteed to have a null terminator. // This will assign the value '\0' to the variable 'ch'. char ch = pStr[str.Length]; } fixed (char* pSpan = span) { // INCORRECT: Spans aren't guaranteed to be null-terminated. // This could throw, assign garbage data to 'ch', or cause an AV and crash. char ch = pSpan[span.Length]; } fixed (char* pArray = array) { // INCORRECT: Arrays aren't guaranteed to be null-terminated. // This could throw, assign garbage data to 'ch', or cause an AV and crash. char ch = pArray[array.Length]; } }❌ Ne adjon át egy rögzített
Span<char>vagyReadOnlySpan<char>a p/invoke határon, hacsak nem ad át egy explicit hosszargumentumot. Ellenkező esetben előfordulhat, hogy a p/invoke határ másik oldalán lévő kód helytelenül úgy véli, hogy a puffer null értékű.
unsafe static extern void SomePInvokeMethod(char* pwszData);
unsafe void IncorrectPInvokeExample(ReadOnlySpan<char> data)
{
fixed (char* pData = data)
{
// INCORRECT: Since 'data' is a span and is not guaranteed to be null-terminated,
// the receiver might attempt to keep reading beyond the end of the buffer,
// resulting in undefined behavior.
SomePInvokeMethod(pData);
}
}
A probléma megoldásához használjon egy másik p/invoke aláírást, amely elfogadja az adatmutatót és a hosszt is, ha lehetséges. Ellenkező esetben, ha a fogadó nem tud elfogadni egy külön hosszargumentumot, győződjön meg arról, hogy az eredeti adatokat string-re alakítják át a rögzítés előtt, majd továbbítják a p/invoke határán.
unsafe static extern void SomePInvokeMethod(char* pwszData);
unsafe static extern void SomePInvokeMethodWhichTakesLength(char* pwszData, uint cchData);
unsafe void CorrectPInvokeExample(ReadOnlySpan<char> data)
{
fixed (char* pData = data)
{
// OK: Since the receiver accepts an explicit length argument, they're signaling
// to us that they don't expect the pointer to point to a null-terminated buffer.
SomePInvokeMethodWhichTakesLength(pData, (uint)data.Length);
}
// Alternatively, if the receiver doesn't accept an explicit length argument, use
// ReadOnlySpan<T>.ToString to convert the data to a null-terminated string before
// pinning it and sending it across the p/invoke boundary.
fixed (char* pStr = data.ToString())
{
// OK: Strings are guaranteed to be null-terminated.
SomePInvokeMethod(pStr);
}
}
17. Sztringmutációk
A C#-sztringek tervezés szerint nem módosíthatók, és a nem biztonságos kóddal történő mutáció bármilyen kísérlete meghatározatlan viselkedéshez vezethet. Példa:
string s = "Hello";
fixed (char* p = s)
{
p[0] = '_';
}
Console.WriteLine("Hello"); // prints "_ello" instead of "Hello"
Az internált sztringek módosítása (a legtöbb sztringkonstans) minden más felhasználási célra megváltoztatja az értéket. Az újonnan létrehozott sztringbe való írást a biztonságosabb String.Create API-val még sztringek közötti internálás nélkül is le kell cserélni:
// Bad:
string s = new string('\n', 4); // non-interned string
fixed (char* p = s)
{
// Copy data into the newly created string
}
// Good:
string s = string.Create(4, state, (chr, state) =>
{
// Copy data into the newly created string
});
Recommendations
-
❌ NEM módosítsa a karakterláncokat.
String.CreateHa összetett másolási logikára van szükség, az API használatával hozzon létre egy új sztringet. Ellenkező esetben használjon.ToString(),StringBuilder,new string(...)vagy sztring interpolációs szintaxist.
18. Nyers IL-kód (például System.Reflection.Emit és Mono.Cecil)
Nyers IL kibocsátása (akár System.Reflection.Emit külső kódtárak, például Mono.Cecil, vagy az IL kód közvetlen írása révén) definíció szerint megkerüli a C# által biztosított memóriabiztonsági garanciákat.
Kerülje az ilyen technikák használatát, kivéve, ha feltétlenül szükséges.
Recommendations
- ❌ NE generáljon nyers IL-kódot, mert nincsenek védőkorlátok, így könnyen felléphetnek típusbiztonsági és egyéb problémák. A többi dinamikus kódgenerálási technikához hasonlóan a nyers IL-kibocsátás sem AOT-barát, ha nem történik meg a létrehozáskor.
- ✔️ Ha lehetséges, használja a forrásgenerátorokat.
- ✔️ DO használja inkább a [UnsafeAccessor] annotációt ahelyett, hogy nyers IL-t írna alacsony terhelésű szerializálási kódhoz, hogy privát tagokat dolgozzon fel, ha szükséges.
- ✔️ Ha hiányzik egy API, és ehelyett nyers IL-kódot kell használnia, feltétlenül nyújtson be egy API-javaslatot a dotnet/runtime ellen.
- ✔️ HASZNÁLJON
ilverifyvagy hasonló eszközöket a kibocsátott IL-kód ellenőrzéséhez, ha nyers IL-t kell használnia.
19. Inicializálatlan helyi változók [SkipLocalsInit] és Unsafe.SkipInit
[SkipLocalsInit] a .NET 5.0-ban vezették be, hogy a JIT kihagyhassa a helyi változók nullázását metódusonként vagy modulszintűen. Ezt a funkciót gyakran használták arra, hogy segítse a JIT-et a redundáns nulla inicializációk, például a stackalloc eliminálásában. Ez azonban meghatározatlan viselkedéshez vezethet, ha a helyi beállításokat a használat előtt nem inicializálják explicit módon. A JIT zéró inicializálások eltávolítására és vektorizációs képeségeinek legutóbbi fejlesztései miatt jelentősen csökkent [SkipLocalsInit] és Unsafe.SkipInit iránti igény.
Recommendations
-
❌ NE használja a
[SkipLocalsInit]-t ésUnsafe.SkipInit-t, ha nem figyel meg teljesítménybeli előnyöket a forró kódban, vagy nem biztos abban, hogy milyen kockázatokat jelentenek. - ✔️ A DO-kód védekező módon használható az olyan API-k
GC.AllocateUninitializedArrayhasználata esetén, mint az ésArrayPool<T>.Shared.Rent, amelyek hasonló módon adnak vissza nem inicializált puffereket.
20. ArrayPool<T>.Shared és hasonló készletezési API-k
ArrayPool<T>.Shared egy megosztott tömbkészlet, amely az intenzíven használt kódban a GC terhelését csökkenti. Gyakran használják ideiglenes pufferek kiosztására I/O-műveletekhez vagy más rövid élettartamú forgatókönyvekhez. Bár az API egyszerű, és nem tartalmaz eleve nem biztonságos funkciókat, a C#-ban használat utáni hibákhoz vezethet. Példa:
var buffer = ArrayPool<byte>.Shared.Rent(1024);
_buffer = buffer; // buffer object escapes the scope
Use(buffer);
ArrayPool<byte>.Shared.Return(buffer);
A hívás utáni _buffer használat Return ingyenes hiba. Ez a minimális példa könnyen észlelhető, de a hiba nehezebben észlelhető, amikor Rent különböző Return hatókörökben vagy metódusokban vannak.
Recommendations
- ✔️ Ha lehetséges, tartsa meg az egyező hívásokat
RentésReturnugyanazon a metóduson belül a lehetséges hibák számának csökkentése érdekében. -
❌NE használjon
try-finallymintát aReturnblokk meghívásáhozfinally, hacsak nem biztos abban, hogy a sikertelen logika befejezte a puffer használatát. Jobb, ha elhagyjuk a puffert, és nem kockáztatunk használat utáni hibát egy váratlan koraiReturnhiba miatt. - ✔️ NE feledje, hogy hasonló problémák merülhetnek fel más készletezési API-kkal vagy mintákkal, például ObjectPool<T>.
21. bool<->int átalakítások
Bár az ECMA-335 szabvány 0–255 értékként definiálja a logikai értékeket, ahol true bármely nullától különböző érték, érdemes elkerülni az egész számok és a logikai értékek közötti explicit konverziókat, hogy elkerülje a "denormalizált" értékek bevezetését, mivel a nullától vagy egytől eltérő értékek valószínűleg megbízhatatlan viselkedéshez vezetnek.
// Bad:
bool b = Unsafe.As<int, bool>(ref someInteger);
int i = Unsafe.As<bool, int>(ref someBool);
// Good:
bool b = (byte)someInteger != 0;
int i = someBool ? 1 : 0;
A korábbi .NET-futtatókörnyezetekben található JIT nem optimalizálta teljes mértékben ennek a logikának a biztonságos verzióját, így a fejlesztők nem biztonságos szerkezeteket használnak a teljesítményérzékeny kódútvonalak közötti és bool azok közötti int konvertáláshoz. Ez már nem így van, és a modern .NET JIT-ek hatékonyan képesek optimalizálni a biztonságos verziót.
Recommendations
- ❌ NE írjon "ág nélküli" konverziókat egész számok és logikai értékek között nem biztonságos kód használatával.
- ✔️ A DO helyett ternáris operátorokat (vagy más elágaztatási logikát) használjon. A modern .NET JIT-k hatékonyan optimalizálják őket.
-
❌ Ne olvassa el
boola nem biztonságos API-kat, példáulUnsafe.ReadUnalignedMemoryMarshal.Castha nem bízik a bemenetben. Fontolja meg inkább a ternáris operátorok vagy egyenlőségi összehasonlítások használatát:
// Bad:
bool b = Unsafe.ReadUnaligned<bool>(ref byteData);
// Good:
bool b = byteData[0] != 0;
// Bad:
ReadOnlySpan<byte> byteSpan = ReadDataFromNetwork();
bool[] boolArray = MemoryMarshal.Cast<byte, bool>(byteSpan).ToArray();
// Good:
ReadOnlySpan<byte> byteSpan = ReadDataFromNetwork();
bool[] boolArray = new bool[byteSpan];
for (int i = 0; i < byteSpan.Length; i++) { boolArray[i] = byteSpan[i] != 0; }
További információ: Szerkezetek bináris (de)szerializálása kitöltésekkel vagy nem Blittable típusú tagokkal.
22. Interop
Bár a dokumentumban szereplő javaslatok többsége az interop forgatókönyvekre is vonatkozik, javasoljuk, hogy kövesse a natív együttműködési ajánlott eljárásokat ismertető útmutatót. Emellett érdemes lehet olyan automatikusan létrehozott interop burkolókat használni, mint a CsWin32 és a CsWinRT. Ez minimálisra csökkenti a manuális interop kód írásának szükségességét, és csökkenti a memóriabiztonsági problémák kockázatát.
23. Szálbiztonság
A memóriabiztonság és a szálbiztonság orgonális fogalmak. Előfordulhat, hogy a kód memóriabiztonságos, de még mindig tartalmazhat adatszál-versenyeket, szakadt olvasásokat vagy láthatósági hibákat; ezzel szemben ugyanaz a kód lehet szálbiztos, miközben kiválthat meg nem határozott viselkedést a nem biztonságos memóriamanipuláció révén. A szélesebb körű útmutatásért tekintse meg a felügyelt szálkezelés ajánlott eljárásait és a .NET memóriamodellt.
24. Nem biztonságos kód a SIMD/Vektorizálás körül
További részletekért tekintse meg a vektorizálási irányelveket . A nem biztonságos kód kontextusában fontos szem előtt tartani:
- A SIMD-műveletek összetett követelményekkel rendelkeznek az atomitási garanciák biztosításához (néha egyáltalán nem biztosítják őket).
- A legtöbb SIMD load/Store API nem biztosít határellenőrzést.
25. Fuzz tesztelés
A Fuzz-tesztelés (vagy "fuzzing") egy automatizált szoftvertesztelési technika, amely érvénytelen, váratlan vagy véletlenszerű adatokat ad meg bemenetként egy számítógépes programnak. Lehetővé teszi olyan memóriabiztonsági problémák észlelését a kódban, amelyeknél a tesztlefedettség esetleg hiányos. A SharpFuzzhoz hasonló eszközökkel beállíthatja a .NET-kód fuzz-tesztelését.
26. Fordítói figyelmeztetések
A C#-fordító általában nem nyújt széles körű támogatást, például figyelmeztetéseket és elemzőket a helytelen, nem biztonságos kódhasználattal kapcsolatosakhoz. Vannak azonban olyan meglévő figyelmeztetések, amelyek segíthetnek a lehetséges problémák észlelésében, és nem hagyhatók figyelmen kívül, és nem mellőzhetők alapos megfontolás nélkül. Néhány példa:
nint ptr = 0;
unsafe
{
int local = 0;
ptr = (nint)(&local);
}
await Task.Delay(100);
// ptr is used here
Ez a kód a CS9123 figyelmeztetést eredményez ("A"> operátor nem használható paramétereken vagy helyi változókon az aszinkron metódusokban"), ami azt jelenti, hogy a kód valószínűleg helytelen.
Recommendations
- ✔️ Figyeljen a fordító figyelmeztetésekre, és javítsa ki a mögöttes problémákat ahelyett, hogy letiltja őket.
- ❌ NE feltételezze, hogy a fordító figyelmeztetéseinek hiánya azt jelenti, hogy a kód helyes. A C#-fordító nem támogatja a helytelen, nem biztonságos kódhasználat észlelését.
Hivatkozások
- Nem biztonságos kód, mutatótípusok és függvénymutatók.
- Nem biztonságos kód, nyelvi specifikáció.
- Amit minden CLR-fejlesztőnek tudnia kell, mielőtt kódokat ír a CoreCLR és a GC belső elemeinek speciális témaköreihez.
- Natív együttműködési ajánlott eljárások.
- A felügyelt szálkezelés ajánlott eljárásai.
- Ajánlott eljárások a kivételekhez.
- Vektorizálási irányelvek
- .NET memóriamodell
- ECMA-335
- ECMA-335 bővítmények