Nem biztonságos kód, mutatótípusok és függvénymutatók
Az ön által írt C#-kód nagy része "ellenőrizhetően biztonságos kód". Ellenőrizhetően biztonságos kód, azt jelenti, hogy a .NET-eszközök ellenőrizhetik, hogy a kód biztonságos-e. A biztonságos kód általában nem fér hozzá közvetlenül a memóriához mutatókkal. Emellett nem foglal le nyers memóriát. Ehelyett felügyelt objektumokat hoz létre.
A C# egy unsafe
környezetet támogat, amelyben nem ellenőrizhető kódot írhat. Egy unsafe
környezetben a kód használhat mutatókat, lefoglalhat és felszabadíthat memóriablokkokat, és függvénymutatókkal meghívhat metódusokat. A C# nem biztonságos kódja nem feltétlenül veszélyes; Csak olyan kód, amelynek a biztonságát nem lehet ellenőrizni.
A nem biztonságos kód a következő tulajdonságokkal rendelkezik:
- A metódusok, típusok és kódblokkok nem biztonságosként definiálhatók.
- Bizonyos esetekben a nem biztonságos kódok növelhetik az alkalmazások teljesítményét a tömbkorlát-ellenőrzések eltávolításával.
- Nem biztonságos kódra van szükség, ha olyan natív függvényeket hív meg, amelyek mutatót igényelnek.
- A nem biztonságos kód használata biztonsági és stabilitási kockázatokat jelent.
- A nem biztonságos blokkokat tartalmazó kódot az AllowUnsafeBlocks fordítóbeállítással kell lefordítani.
Mutatótípusok
Nem biztonságos környezetben a típus lehet mutatótípus, az értéktípuson kívül vagy hivatkozástípus is. A mutatótípus-deklaráció az alábbi űrlapok egyikét használja:
type* identifier;
void* identifier; //allowed but not recommended
A mutatótípus *
előtt megadott típust hivatkozástípusnak nevezzük.
A mutatótípusok nem örökölnek objektumtól, és a mutatótípusok és a object
között nincs átalakítás. Emellett a boxolás és a kicsomagolás nem támogatja a mutatók használatát. A különböző mutatótípusok, valamint a mutatótípusok és az integráltípusok között azonban konvertálható.
Ha ugyanabban a deklarációban több mutatót deklarál, a csillagot (*
) csak az alapul szolgáló típussal együtt kell írnia. Nem minden mutatónév előtagjaként használatos. Például:
int* p1, p2, p3; // Ok
int *p1, *p2, *p3; // Invalid in C#
A szemétgyűjtő nem követi nyomon, hogy egy objektumra bármilyen mutató típus mutat-e. Ha a hivatkozott elem egy objektum a kezelt veremben (beleértve a lambda kifejezések vagy névtelen delegáltak által lefoglalt helyi változókat is), az objektumot kitűzöttként kell megjelölni mindaddig, amíg a mutatót használják.
Az MyType*
típusú mutatóváltozó értéke egy MyType
típusú változó címe. Az alábbiakban példákat láthat a mutatótípus-deklarációkra:
-
int* p
:p
egy egész számra mutató mutató. -
int** p
:p
egy egész számra mutató mutató. -
int*[] p
:p
egész számokra mutató mutatók egydimenziós tömbje. -
char* p
:p
egy karakterre mutató. -
void* p
:p
egy ismeretlen típusra mutató mutató.
A mutató indirekt operátora *
a mutatóváltozó által mutatott helyen található tartalom eléréséhez használható. Vegyük például a következő deklarációt:
int* myVariable;
A *myVariable
kifejezés a myVariable
címében található int
változót jelöli.
A fixed
utasításcímű cikkekben számos példa található a mutatókra. Az alábbi példa a unsafe
kulcsszót és a fixed
utasítást használja, és bemutatja, hogyan lehet növelni a belső mutatót. Ezt a kódot beillesztheti egy konzolalkalmazás fő függvényébe a futtatáshoz. Ezeket a példákat a AllowUnsafeBlocks fordítóbeállításkészlettel kell lefordítani.
// Normal pointer to an object.
int[] a = [10, 20, 30, 40, 50];
// Must be in unsafe code to use interior pointers.
unsafe
{
// Must pin object on heap so that it doesn't move while using interior pointers.
fixed (int* p = &a[0])
{
// p is pinned as well as object, so create another pointer to show incrementing it.
int* p2 = p;
Console.WriteLine(*p2);
// Incrementing p2 bumps the pointer by four bytes due to its type ...
p2 += 1;
Console.WriteLine(*p2);
p2 += 1;
Console.WriteLine(*p2);
Console.WriteLine("--------");
Console.WriteLine(*p);
// Dereferencing p and incrementing changes the value of a[0] ...
*p += 1;
Console.WriteLine(*p);
*p += 1;
Console.WriteLine(*p);
}
}
Console.WriteLine("--------");
Console.WriteLine(a[0]);
/*
Output:
10
20
30
--------
10
11
12
--------
12
*/
Az indirekt operátor nem alkalmazható void*
típusú mutatóra. Az üres mutatót azonban bármely más típusú mutatóvá alakíthatja át, és fordítva.
A mutató lehet null
. Ha a közvetett operátort null mutatóra alkalmazza, az implementáció által definiált viselkedést okoz.
Ha mutatót ad át a metódusok között, az meghatározatlan viselkedést okozhat. Fontolja meg azt a metódust, amely egy helyi változóra mutató mutatót ad vissza egy in
, out
vagy ref
paraméteren keresztül, vagy a függvény eredményeként. Ha a mutató rögzített blokkban volt beállítva, előfordulhat, hogy a változó, amelyre mutat, már nem lesz javítva.
Az alábbi táblázat felsorolja azokat az operátorokat és utasításokat, amelyek nem biztonságos környezetben működnek a mutatókon:
Operátor/utasítás | Használ |
---|---|
* |
Mutató indirekciót hajt végre. |
-> |
Egy struktúra egy tagját egy mutatón keresztül éri el. |
[] |
Indexel egy mutatót. |
& |
Egy változó címét szerzi be. |
++ és -- |
Növekmények és csökkenő mutatók. |
+ és - |
Mutató aritmetikai műveleteket végez. |
== , != , < , > , <= és >= |
Összehasonlítja a mutatókat. |
stackalloc |
Memóriát foglal le a veremen. |
fixed nyilatkozat |
Ideiglenesen kijavít egy változót, hogy a címe megtalálható legyen. |
További információ a mutatóval kapcsolatos operátorokról: Mutatóval kapcsolatos operátorok.
Bármely mutatótípus implicit módon konvertálható void*
típussá. Bármely mutatótípus hozzárendelhető a null
értékhez. Bármely mutatótípus explicit módon bármely más mutatótípussá alakítható át egy öntött kifejezés használatával. Bármely integráltípust átalakíthat mutatótípussá, vagy bármely mutatótípust integráltípussá. Ezek az átalakítások explicit leadást igényelnek.
Az alábbi példa egy int*
értéket alakít át byte*
értékké. Figyelje meg, hogy a mutató a változó legalacsonyabb címzett bájtjára mutat. Az eredmény egymást követő növekménye esetén a int
(4 bájt) méretig megjelenítheti a változó fennmaradó bájtját.
int number = 1024;
unsafe
{
// Convert to byte:
byte* p = (byte*)&number;
System.Console.Write("The 4 bytes of the integer:");
// Display the 4 bytes of the int variable:
for (int i = 0 ; i < sizeof(int) ; ++i)
{
System.Console.Write(" {0:X2}", *p);
// Increment the pointer:
p++;
}
System.Console.WriteLine();
System.Console.WriteLine("The value of the integer: {0}", number);
/* Output:
The 4 bytes of the integer: 00 04 00 00
The value of the integer: 1024
*/
}
Rögzített méretű pufferek
A fixed
kulcsszóval egy rögzített méretű tömböt tartalmazó puffert hozhat létre egy adatstruktúrában. A rögzített méretű pufferek akkor hasznosak, ha olyan metódusokat ír, amelyek más nyelvekből vagy platformokról származó adatforrásokkal működnek együtt. A rögzített méretű puffer bármilyen attribútumot vagy módosító tulajdonságot képes átvenni, amely a normál strukturált tagok számára engedélyezett. Az egyetlen korlátozás, hogy a tömbtípusnak bool
, byte
, char
, short
, int
, long
, sbyte
, ushort
, uint
, ulong
, float
vagy double
kell lennie.
private fixed char name[30];
A biztonságos kódban a tömböt tartalmazó C# szerkezet nem tartalmazza a tömbelemeket. A szerkezet ehelyett az elemekre mutató hivatkozást tartalmaz. Rögzített méretű tömböt beágyazhat egy struktúrába, amikor az nem biztonságos kódblokkban van használva.
A következő struct
mérete nem függ a tömb elemeinek számától, mivel pathName
hivatkozás:
public struct PathArray
{
public char[] pathName;
private int reserved;
}
A struktúra tartalmazhat beágyazott tömböt nem biztonságos kódban. Az alábbi példában a fixedBuffer
tömb mérete rögzített. Egy fixed
utasítással az első elemre mutató mutatót kap. Ezen a mutatón keresztül érheti el a tömb elemeit. A fixed
utasítás a fixedBuffer
példánymezőt egy adott helyre rögzíti a memóriában.
internal unsafe struct Buffer
{
public fixed char fixedBuffer[128];
}
internal unsafe class Example
{
public Buffer buffer = default;
}
private static void AccessEmbeddedArray()
{
var example = new Example();
unsafe
{
// Pin the buffer to a fixed location in memory.
fixed (char* charPtr = example.buffer.fixedBuffer)
{
*charPtr = 'A';
}
// Access safely through the index:
char c = example.buffer.fixedBuffer[0];
Console.WriteLine(c);
// Modify through the index:
example.buffer.fixedBuffer[0] = 'B';
Console.WriteLine(example.buffer.fixedBuffer[0]);
}
}
A 128 elem char
tömb mérete 256 bájt. A rögzített méretű karakteres pufferek karakterenként mindig 2 bájtot vesznek igénybe, a kódolástól függetlenül. Ez a tömbméret akkor is megegyezik, ha a karaktertárolók API metódusokhoz vagy struktúrákhoz történő átadáskor az CharSet = CharSet.Auto
vagy CharSet = CharSet.Ansi
paraméterekkel történnek. További információ: CharSet.
Az előző példa bemutatja fixed
mezők rögzítés nélküli elérését. Egy másik gyakori rögzített méretű tömb a bool tömb. A bool
tömb elemei mindig 1 bájt méretűek.
bool
tömbök nem alkalmasak bittömbök vagy pufferek létrehozására.
A rögzített méretű pufferek a System.Runtime.CompilerServices.UnsafeValueTypeAttributedirektívával vannak lefordítva, amely arra utasítja a közös nyelvi futtatókörnyezetet (CLR), hogy egy típus egy nem felügyelt tömböt tartalmaz, amely esetleg túlcsordulhat. A stackalloc használatával lefoglalt memória automatikusan engedélyezi a puffertúllépés észlelését a CLR-ben. Az előző példa bemutatja, hogyan létezhet rögzített méretű puffer egy unsafe struct
.
internal unsafe struct Buffer
{
public fixed char fixedBuffer[128];
}
A fordító által létrehozott C# kód Buffer
attribútumként van megjelölve a következőképpen:
internal struct Buffer
{
[StructLayout(LayoutKind.Sequential, Size = 256)]
[CompilerGenerated]
[UnsafeValueType]
public struct <fixedBuffer>e__FixedBuffer
{
public char FixedElementField;
}
[FixedBuffer(typeof(char), 128)]
public <fixedBuffer>e__FixedBuffer fixedBuffer;
}
A rögzített méretű pufferek a következő módokon térnek el a normál tömböktől:
- Csak
unsafe
környezetben használható. - Csak a szerkezetek példánymezői lehetnek.
- Ezek mindig vektorok, vagy egydimenziós tömbök.
- A deklarációnak tartalmaznia kell a hosszt, például
fixed char id[8]
. Afixed char id[]
nem használható.
Hogyan használjunk mutatókat egy bájttömb másolásához
Az alábbi példa mutatókkal másol bájtokat az egyik tömbből a másikba.
Ez a példa a nem biztonságos kulcsszót használja, amely lehetővé teszi a mutatók használatát a Copy
metódusban. A rögzített utasítás a forrás- és céltömbök mutatóinak deklarálásához használható. A fixed
utasítás rögzíti a forrás- és céltömbök helyét a memóriában, hogy a szemétgyűjtés ne helyezze át a tömböket. A tömbök memóriablokkjai a fixed
blokk befejezésekor lesznek felszabadítva. Mivel a példában szereplő Copy
metódus a unsafe
kulcsszót használja, le kell fordítani az AllowUnsafeBlocks fordítóbeállítással.
Ez a példa a második nem felügyelt mutató helyett indexekkel fér hozzá mindkét tömb elemeihez. A pSource
és pTarget
mutatóinak deklarációja rögzíti a tömböket.
static unsafe void Copy(byte[] source, int sourceOffset, byte[] target,
int targetOffset, int count)
{
// If either array is not instantiated, you cannot complete the copy.
if ((source == null) || (target == null))
{
throw new System.ArgumentException("source or target is null");
}
// If either offset, or the number of bytes to copy, is negative, you
// cannot complete the copy.
if ((sourceOffset < 0) || (targetOffset < 0) || (count < 0))
{
throw new System.ArgumentException("offset or bytes to copy is negative");
}
// If the number of bytes from the offset to the end of the array is
// less than the number of bytes you want to copy, you cannot complete
// the copy.
if ((source.Length - sourceOffset < count) ||
(target.Length - targetOffset < count))
{
throw new System.ArgumentException("offset to end of array is less than bytes to be copied");
}
// The following fixed statement pins the location of the source and
// target objects in memory so that they will not be moved by garbage
// collection.
fixed (byte* pSource = source, pTarget = target)
{
// Copy the specified number of bytes from source to target.
for (int i = 0; i < count; i++)
{
pTarget[targetOffset + i] = pSource[sourceOffset + i];
}
}
}
static void UnsafeCopyArrays()
{
// Create two arrays of the same length.
int length = 100;
byte[] byteArray1 = new byte[length];
byte[] byteArray2 = new byte[length];
// Fill byteArray1 with 0 - 99.
for (int i = 0; i < length; ++i)
{
byteArray1[i] = (byte)i;
}
// Display the first 10 elements in byteArray1.
System.Console.WriteLine("The first 10 elements of the original are:");
for (int i = 0; i < 10; ++i)
{
System.Console.Write(byteArray1[i] + " ");
}
System.Console.WriteLine("\n");
// Copy the contents of byteArray1 to byteArray2.
Copy(byteArray1, 0, byteArray2, 0, length);
// Display the first 10 elements in the copy, byteArray2.
System.Console.WriteLine("The first 10 elements of the copy are:");
for (int i = 0; i < 10; ++i)
{
System.Console.Write(byteArray2[i] + " ");
}
System.Console.WriteLine("\n");
// Copy the contents of the last 10 elements of byteArray1 to the
// beginning of byteArray2.
// The offset specifies where the copying begins in the source array.
int offset = length - 10;
Copy(byteArray1, offset, byteArray2, 0, length - offset);
// Display the first 10 elements in the copy, byteArray2.
System.Console.WriteLine("The first 10 elements of the copy are:");
for (int i = 0; i < 10; ++i)
{
System.Console.Write(byteArray2[i] + " ");
}
System.Console.WriteLine("\n");
/* Output:
The first 10 elements of the original are:
0 1 2 3 4 5 6 7 8 9
The first 10 elements of the copy are:
0 1 2 3 4 5 6 7 8 9
The first 10 elements of the copy are:
90 91 92 93 94 95 96 97 98 99
*/
}
Függvénymutatók
A C# delegate
típusokat biztosít a biztonságos függvénymutató-objektumok definiálásához. Egy delegált meghívása magában foglalja a System.Delegate-ból származtatott típus példányosítását, valamint egy virtuális metódus meghívását a Invoke
metódusra. Ez a virtuális hívás a callvirt
IL utasítást használja. A teljesítmény szempontjából kritikus kódútvonalak esetén hatékonyabb az calli
IL-utasítás használata.
A függvénymutatót a delegate*
szintaxissal határozhatja meg. A fordító a calli
utasítással hívja meg a függvényt ahelyett, hogy egy delegate
objektumot hozna létre, és meghívja Invoke
. Az alábbi kód két metódust deklarál, amelyek egy delegate
vagy egy delegate*
használnak két azonos típusú objektum kombinálásához. Az első metódus egy System.Func<T1,T2,TResult> delegált típust használ. A második módszer egy delegate*
deklarációt használ ugyanazokkal a paraméterekkel és visszatérési típussal:
public static T Combine<T>(Func<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static unsafe T UnsafeCombine<T>(delegate*<T, T, T> combinator, T left, T right) =>
combinator(left, right);
Az alábbi kód bemutatja, hogyan deklarálna egy statikus helyi függvényt, és hogyan hívná meg a UnsafeCombine
metódust az adott helyi függvényre mutató mutatóval:
int product = 0;
unsafe
{
static int localMultiply(int x, int y) => x * y;
product = UnsafeCombine(&localMultiply, 3, 4);
}
Az előző kód a függvénymutatóként elért függvény számos szabályát szemlélteti:
- A függvénymutatók csak
unsafe
környezetben deklarálhatók. - A
delegate*
típusú (vagy visszaad egydelegate*
) metódusok csakunsafe
környezetben hívhatók meg. - A függvény címének lekérésére
&
operátor csakstatic
függvényeken engedélyezett. (Ez a szabály a tagfüggvényekre és a helyi függvényekre is vonatkozik).
A szintaxis párhuzamot mutat az delegate
típus deklarálásával és a mutatók használatával. A *
utótag a delegate
-n azt jelzi, hogy a deklaráció egy függvénymutató. A &
, amikor metóduscsoportot rendel egy függvénymutatóhoz, azt jelzi, hogy a művelet a metódus címét veszi át.
A delegate*
hívási konvenciójának megadásához használja a managed
és a unmanaged
kulcsszavakat. Emellett unmanaged
függvénymutatók esetében megadhatja a hívási konvencióciót. Az alábbi deklarációk példákat mutatnak mindegyikre. Az első deklaráció a managed
hívási konvenciót használja, amely az alapértelmezett. A következő négy egy unmanaged
hívási konvenciót használ. Mindegyik megadja az ECMA 335 hívási konvencióinak egyikét: Cdecl
, Stdcall
, Fastcall
vagy Thiscall
. Az utolsó deklaráció a unmanaged
hívási konvenciót használja, amely arra utasítja a CLR-t, hogy válassza ki a platform alapértelmezett hívási konvenciót. A CLR futásidőben választja ki a hívási konvenciót.
public static unsafe T ManagedCombine<T>(delegate* managed<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static unsafe T CDeclCombine<T>(delegate* unmanaged[Cdecl]<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static unsafe T StdcallCombine<T>(delegate* unmanaged[Stdcall]<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static unsafe T FastcallCombine<T>(delegate* unmanaged[Fastcall]<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static unsafe T ThiscallCombine<T>(delegate* unmanaged[Thiscall]<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static unsafe T UnmanagedCombine<T>(delegate* unmanaged<T, T, T> combinator, T left, T right) =>
combinator(left, right);
A függvénymutatókról a függvénymutató funkciós specifikációjában olvashat bővebben.
C# nyelvspecifikáció
További információt a C# nyelvi specifikációjánaknem biztonságos kód fejezetében talál.