Share via


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" .Az 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# olyan unsafe környezetet támogat, amelyben ellenőrizhető kódot írhat. unsafe Egy környezetben a kód használhat mutatókat, lefoglalhat és felszabadíthat memóriablokkokat, és függvénymutatókat használó metódusokat hívhat meg. A C# nem biztonságos kódja nem feltétlenül veszélyes; csak olyan kód, amelynek 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ód növelheti 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. Csak a nem felügyelt típus lehet hivatkozási típus.

A mutatótípusok nem öröklődnek az objektumtól, és nem léteznek átalakítások a mutatótípusok és a objectmutatótípusok között. Emellett a boxolás és a kicsomagolás nem támogatja a mutatókat. 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élda:

int* p1, p2, p3;   // Ok
int *p1, *p2, *p3;   // Invalid in C#

A mutató nem mutathat hivatkozásra vagy hivatkozásokat tartalmazó szerkezetre , mert az objektumhivatkozások akkor is szemétként gyűjthetők, ha egy mutató erre mutat. A szemétgyűjtő nem követi nyomon, hogy egy objektumra mutató mutató típusok mutatnak-e.

A mutató típusú MyType* változó értéke egy típusváltozó MyTypecí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 az egész számokra mutató mutatók egydimenziós tömbje.
  • char* p: p egy karakterre mutató mutató.
  • void* p: p ismeretlen típusra mutató mutató.

A mutató indirekt operátora * a mutatóváltozó által mutatott helyen lévő tartalom eléréséhez használható. Vegyük például a következő deklarációt:

int* myVariable;

A kifejezés *myVariable a int megadott címen myVariabletalálható változót jelöli.

Az utasításban szereplő cikkekben számos példa mutat.fixed Az alábbi példa a kulcsszót unsafe és az utasítást fixed 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 az AllowUnsafeBlocks fordítóké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ó egy ilyen típusú void*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, outvagy paraméteren ref keresztül, vagy a függvény eredményeként. Ha az egérmutató rögzített blokkban volt beállítva, előfordulhat, hogy a változó, amelyre mutat, többé 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álat
* Indirekt mutató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 Nyilatkozatot Ideiglenesen kijavít egy változót, hogy a címe megtalálható legyen.

A mutatóval kapcsolatos operátorokról további információt a Mutatóval kapcsolatos operátorok című témakörben talál.

Bármely mutatótípus implicit módon átalakítható típussá void* . Az érték nullbármely mutatótípushoz hozzárendelhető. 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*byte*. Figyelje meg, hogy a mutató a változó legalacsonyabb címzett bájtjára mutat. Ha egymás után növeli az eredményt, akár a (4 bájtos) méretig int , 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 kulcsszóval fixed 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 az, hogy a tömbtípusnak boolbyte, , , char, short, longintsbyte, , ushort, uint, ulongfloatdouble, vagy .

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. Ha nem biztonságos kódblokkban használják, rögzített méretű tömböt ágyazhat be egy szerkezetbe.

A következők struct mérete nem függ a tömb elemeinek számától, mivel pathName ez egy hivatkozás:

public struct PathArray
{
    public char[] pathName;
    private int reserved;
}

A szerkezetek nem biztonságos kódba ágyazott tömböt tartalmazhatnak. Az alábbi példában a fixedBuffer tömb mérete rögzített. Egy utasítással fixed mutatót kap az első elemhez. Ezen a mutatón keresztül érheti el a tömb elemeit. Az fixed utasítás a példánymezőt fixedBuffer 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 elemtömb char mérete 256 bájt. A rögzített méretű karakterpufferek 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 karakterpufferek API-metódusokhoz vannak rendezve, vagy azokkal vagy CharSet = CharSet.Ansi.CharSet = CharSet.Auto További információ: CharSet.

Az előző példa a mezők rögzítés nélküli elérését fixed mutatja be. Egy másik gyakori rögzített méretű tömb a bool tömb. A tömb elemei bool mindig 1 bájt méretűek. bool a 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.UnsafeValueTypeAttributekövetkezővel vannak lefordítva, ami 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# a Buffer következőképpen van attribútummal elosztva:

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 kontextusban unsafe 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]. Nem használható fixed char id[].

Mutató használata bájtok tömbjének 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ó. Az fixed utasítás rögzíti a forrás- és céltömbök helyét a memóriában, hogy ne helyezhesse át őket szemétgyűjtés. A tömbök memóriablokkjai a blokk befejezésekor fixed nem lesznek rögzítve. Mivel a példában szereplő Copy metódus a kulcsszót unsafe használja, az AllowUnsafeBlocks fordítóval kell lefordítani.

Ez a példa a második nem felügyelt mutató helyett indexekkel fér hozzá mindkét tömb elemeihez. Az és pTarget a pSource mutatók 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# típusokat biztosít delegate a biztonságos függvénymutató-objektumok definiálásához. A meghatalmazott meghívása magában foglalja a metódusból System.Delegate származtatott típus példányosítását, és virtuális metódus hívását Invoke . Ez a virtuális hívás az IL utasítást callvirt használja. A teljesítmény szempontjából kritikus kódelérési utaknál hatékonyabb az calli IL-utasítás használata.

Függvénymutatót a delegate* szintaxissal határozhat meg. A fordító az utasítással hívja meg a függvényt delegate az calli objektum példányosítása és hívása Invokehelyett. Az alábbi kód két metódust deklarál, amelyek két delegate azonos típusú objektumot kombinálnak egy vagy egy delegate* használatával. Az első metódus delegált típust System.Func<T1,T2,TResult> használ. A második módszer ugyanazokkal a paraméterekkel és visszatérési típussal rendelkező deklarációt használ delegate* :

public static T Combine<T>(Func<T, T, T> combinator, T left, T right) => 
    combinator(left, right);

public static T UnsafeCombine<T>(delegate*<T, T, T> combinator, T left, T right) => 
    combinator(left, right);

Az alábbi kód bemutatja, hogyan deklarálhat egy statikus helyi függvényt, és hogyan hívhatja meg a UnsafeCombine metódust az adott helyi függvényre mutató mutatóval:

static int localMultiply(int x, int y) => x * y;
int 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 kontextusban unsafe deklarálhatók.
  • delegate* A (vagy visszaadottdelegate*) metódusokat csak kontextusban unsafe lehet meghívni.
  • A & függvény címét beolvasni kívánt operátor csak függvényeken static engedélyezett. (Ez a szabály a tagfüggvényekre és a helyi függvényekre is vonatkozik).

A szintaxis párhuzamot mutat a deklarálási delegate típusokkal és a mutatókkal. A * deklaráció utótagja delegate egy függvénymutató. A & metóduscsoport függvénymutatóhoz rendelése azt jelzi, hogy a művelet a metódus címét veszi át.

Megadhatja a hívási konvenciót delegate* a kulcsszavak managed és unmanageda . Emellett a függvénymutatók esetében unmanaged megadhatja a hívási konvencióciót. Az alábbi deklarációk példákat mutatnak mindegyikre. Az első deklaráció a hívási managed konvenciót használja, amely az alapértelmezett. A következő négy egy hívási konvenciót unmanaged használ. Mindegyik az ECMA 335 hívási konvenciók egyikét adja meg: Cdecl, Stdcallvagy ThiscallFastcall. Az utolsó deklaráció a hívási unmanaged 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 T ManagedCombine<T>(delegate* managed<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static T CDeclCombine<T>(delegate* unmanaged[Cdecl]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static T StdcallCombine<T>(delegate* unmanaged[Stdcall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static T FastcallCombine<T>(delegate* unmanaged[Fastcall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static T ThiscallCombine<T>(delegate* unmanaged[Thiscall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static 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ó specifikációjában olvashat bővebben.

C# nyelvspecifikáció

További információ: A C# nyelv specifikációjának nem biztonságos kód fejezete.