Onveilige code, aanwijzertypen en functie-aanwijzers

De meeste C#-code die u schrijft, is 'verifieerbaar veilige code'. Verifieerbare veilige code betekent dat .NET-hulpprogramma's kunnen controleren of de code veilig is. Over het algemeen heeft veilige code geen rechtstreeks toegang tot geheugen met behulp van aanwijzers. Er wordt ook geen onbewerkt geheugen toegewezen. In plaats daarvan worden beheerde objecten gemaakt.

C# ondersteunt een unsafe context waarin u niet-verifieerbare code kunt schrijven. In een unsafe context kan code pointers gebruiken, geheugenblokken toewijzen en vrijmaken en methoden aanroepen met behulp van functieaanwijzers. Onveilige code in C# is niet noodzakelijkerwijs gevaarlijk; het is gewoon code waarvan de veiligheid niet kan worden geverifieerd.

Onveilige code heeft de volgende eigenschappen:

  • Methoden, typen en codeblokken kunnen worden gedefinieerd als onveilig.
  • In sommige gevallen kan onveilige code de prestaties van een toepassing verhogen door matrixgrenzencontroles te verwijderen.
  • Onveilige code is vereist wanneer u systeemeigen functies aanroept waarvoor aanwijzers zijn vereist.
  • Het gebruik van onveilige code introduceert beveiligings- en stabiliteitsrisico's.
  • De code die onveilige blokken bevat, moet worden gecompileerd met de optie AllowUnsafeBlocks Compiler.

Aanwijzertypen

In een onveilige context kan een type een aanwijzer zijn, naast een waardetype of een verwijzingstype. Een declaratie van het type aanwijzer heeft een van de volgende formulieren:

type* identifier;
void* identifier; //allowed but not recommended

Het type dat is opgegeven voor het * type aanwijzer, wordt het verwijzingstype genoemd. Alleen een niet-beheerd type kan een referenttype zijn.

Aanwijzertypen nemen niet over van object en er bestaan geen conversies tussen aanwijzertypen en object. Bovendien bieden boksen en uitpakken geen ondersteuning voor aanwijzers. U kunt echter converteren tussen verschillende typen aanwijzers en tussen aanwijzertypen en integrale typen.

Wanneer u meerdere aanwijzers in dezelfde declaratie declareert, schrijft u het sterretje (*) samen met het onderliggende type. Het wordt niet gebruikt als voorvoegsel voor elke aanwijzernaam. Voorbeeld:

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

Een aanwijzer kan niet verwijzen naar een verwijzing of naar een struct die verwijzingen bevat, omdat een objectverwijzing kan worden verzameld, zelfs niet als een aanwijzer ernaar wijst. De garbagecollector houdt niet bij of een object wordt verwezen door eventuele aanwijzertypen.

De waarde van de aanwijzervariabele van het type MyType* is het adres van een variabele van het type MyType. Hier volgen voorbeelden van declaraties van aanwijzertypen:

  • int* p: p is een aanwijzer naar een geheel getal.
  • int** p: p is een aanwijzer naar een aanwijzer naar een geheel getal.
  • int*[] p: p is een eendimensionale matrix van aanwijzers naar gehele getallen.
  • char* p: p is een aanwijzer naar een teken.
  • void* p: p is een aanwijzer naar een onbekend type.

De operator * voor aanwijzer indirectie kan worden gebruikt om toegang te krijgen tot de inhoud op de locatie die wordt verwezen door de aanwijzervariabele. Denk bijvoorbeeld aan de volgende declaratie:

int* myVariable;

De expressie *myVariable geeft de int variabele aan die is gevonden op het adres in myVariable.

Er zijn verschillende voorbeelden van aanwijzers in de artikelen over de fixed instructie. In het volgende voorbeeld worden het unsafe trefwoord en de fixed instructie gebruikt en ziet u hoe u een binnenaanwijzer kunt verhogen. U kunt deze code in de hoofdfunctie van een consoletoepassing plakken om deze uit te voeren. Deze voorbeelden moeten worden gecompileerd met de optieset AllowUnsafeBlocks Compiler.

// 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
*/

U kunt de indirectieoperator niet toepassen op een aanwijzer van het type void*. U kunt echter een cast gebruiken om een ongeldige aanwijzer te converteren naar elk ander type aanwijzer en omgekeerd.

Een aanwijzer kan zijn null. Het toepassen van de indirectieoperator op een null-aanwijzer veroorzaakt een door de implementatie gedefinieerd gedrag.

Het doorgeven van aanwijzers tussen methoden kan niet-gedefinieerd gedrag veroorzaken. Overweeg een methode die een aanwijzer retourneert naar een lokale variabele via een in, outof ref parameter of als het functieresultaat. Als de aanwijzer is ingesteld in een vast blok, kan de variabele waarop de aanwijzer wijst, niet meer worden opgelost.

De volgende tabel bevat de operators en instructies die kunnen worden uitgevoerd op aanwijzers in een onveilige context:

Operator/instructie Gebruik
* Voert aanwijzer indirectie uit.
-> Verwijst naar een lid van een struct via een aanwijzer.
[] Indexeert een aanwijzer.
& Hiermee haalt u het adres van een variabele op.
++ en -- Aanwijzers verhogen en verlagen.
+ en - Hiermee voert u rekenkundige aanwijzers uit.
==, , !=<, >, , , en <=>= Hiermee worden aanwijzers vergeleken.
stackalloc Wijst geheugen toe aan de stack.
fixed Verklaring Corrigeert tijdelijk een variabele, zodat het adres ervan kan worden gevonden.

Zie Pointer-gerelateerde operators voor meer informatie over aanwijzers.

Elk type aanwijzer kan impliciet worden geconverteerd naar een void* type. Elk type aanwijzer kan aan de waarde nullworden toegewezen. Elk type aanwijzer kan expliciet worden geconverteerd naar elk ander type aanwijzer met behulp van een cast-expressie. U kunt ook elk integraal type converteren naar een aanwijzer of een type aanwijzer naar een integraal type. Voor deze conversies is een expliciete cast vereist.

In het volgende voorbeeld wordt een int* geconverteerd naar een byte*. U ziet dat de aanwijzer verwijst naar de laagst geadresseerde byte van de variabele. Wanneer u het resultaat achtereenvolgens vergroot tot de grootte van int (4 bytes), kunt u de resterende bytes van de variabele weergeven.

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
    */
}

Buffers met vaste grootte

U kunt het fixed trefwoord gebruiken om een buffer te maken met een matrix met vaste grootte in een gegevensstructuur. Buffers met een vaste grootte zijn handig wanneer u methoden schrijft die samenwerken met gegevensbronnen uit andere talen of platforms. De buffer met vaste grootte kan alle kenmerken of modifiers aannemen die zijn toegestaan voor gewone struct-leden. De enige beperking is dat het matrixtype moet zijnbool: , byte, char, short, , longint, ushortuintsbyte, ulong, of floatdouble.

private fixed char name[30];

In veilige code bevat een C#-struct die een matrix bevat niet de matrixelementen. De struct bevat in plaats daarvan een verwijzing naar de elementen. U kunt een matrix met vaste grootte insluiten in een struct wanneer deze wordt gebruikt in een onveilig codeblok.

De grootte van het volgende struct is niet afhankelijk van het aantal elementen in de matrix, omdat pathName dit een verwijzing is:

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

Een struct kan een ingesloten matrix bevatten in onveilige code. In het volgende voorbeeld heeft de fixedBuffer matrix een vaste grootte. U gebruikt een fixed instructie om een aanwijzer naar het eerste element te krijgen. U opent de elementen van de matrix via deze aanwijzer. Met fixed de instructie wordt het exemplaarveld vastgemaakt aan een specifieke locatie in het fixedBuffer geheugen.

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

De grootte van de matrix met 128 elementen char is 256 bytes. Tekenbuffers met vaste grootte nemen altijd 2 bytes per teken in beslag, ongeacht de codering. Deze matrixgrootte is hetzelfde, zelfs wanneer tekenbuffers worden ge marshalld naar API-methoden of structs met CharSet = CharSet.Auto of CharSet = CharSet.Ansi. Zie CharSet voor meer informatie.

In het voorgaande voorbeeld ziet u hoe fixed u velden opent zonder vast te maken. Een andere veelgebruikte matrix met vaste grootte is de boolmatrix . De elementen in een bool matrix zijn altijd 1 byte groot. bool matrices zijn niet geschikt voor het maken van bitmatrices of buffers.

Buffers met vaste grootte worden gecompileerd met de System.Runtime.CompilerServices.UnsafeValueTypeAttribute, waarmee de COMMON Language Runtime (CLR) wordt geïnstrueerd dat een type een onbeheerde matrix bevat die mogelijk overloop kan hebben. Geheugen toegewezen met behulp van stackalloc schakelt ook automatisch detectiefuncties voor bufferoverschrijding in de CLR in. In het vorige voorbeeld ziet u hoe een buffer met een vaste grootte in een unsafe struct.

internal unsafe struct Buffer
{
    public fixed char fixedBuffer[128];
}

De door de compiler gegenereerde C# wordt Buffer als volgt toegeschreven:

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

Buffers met vaste grootte verschillen van reguliere matrices op de volgende manieren:

  • Mag alleen in een unsafe context worden gebruikt.
  • Dit kunnen alleen instantievelden van structs zijn.
  • Ze zijn altijd vectoren of eendimensionale matrices.
  • De aangifte moet de lengte bevatten, zoals fixed char id[8]. U kunt het niet gebruiken fixed char id[].

Aanwijzers gebruiken om een matrix van bytes te kopiëren

In het volgende voorbeeld worden aanwijzers gebruikt om bytes van de ene matrix naar de andere te kopiëren.

In dit voorbeeld wordt het onveilige trefwoord gebruikt, waarmee u aanwijzers in de Copy methode kunt gebruiken. De vaste instructie wordt gebruikt om aanwijzers naar de bron- en doelmatrices te declareren. Met fixed de instructie wordt de locatie van de bron- en doelmatrices in het geheugen vastgemaakt , zodat ze niet worden verplaatst door garbagecollection. De geheugenblokken voor de matrices worden losgemaakt wanneer het fixed blok is voltooid. Omdat de Copy methode in dit voorbeeld gebruikmaakt van het unsafe trefwoord, moet deze worden gecompileerd met de optie AllowUnsafeBlocks compiler.

In dit voorbeeld worden de elementen van beide matrices geopend met behulp van indexen in plaats van een tweede onbeheerde aanwijzer. De declaratie van de pSource en pTarget aanwijzers maakt de matrices vast.

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
    */
}

Functie-aanwijzers

C# biedt delegate typen voor het definiëren van veilige functiepointerobjecten. Het aanroepen van een gemachtigde omvat het instantiëren van een type dat is afgeleid van System.Delegate en het aanroepen van een virtuele methode naar de Invoke bijbehorende methode. Deze virtuele aanroep maakt gebruik van de callvirt IL-instructie. In prestatiekritieke codepaden is het gebruik van de calli IL-instructie efficiënter.

U kunt een functiepointer definiëren met behulp van de delegate* syntaxis. De compiler roept de functie aan met behulp van de calli instructie in plaats van een delegate object te instantiëren en aan te roepen Invoke. De volgende code declareert twee methoden die gebruikmaken van een delegate of a delegate* om twee objecten van hetzelfde type te combineren. De eerste methode maakt gebruik van een System.Func<T1,T2,TResult> gemachtigdentype. De tweede methode gebruikt een delegate* declaratie met dezelfde parameters en retourtype:

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

De volgende code laat zien hoe u een statische lokale functie declareert en de UnsafeCombine methode aanroept met behulp van een aanwijzer naar die lokale functie:

static int localMultiply(int x, int y) => x * y;
int product = UnsafeCombine(&localMultiply, 3, 4);

De voorgaande code illustreert een aantal van de regels voor de functie die als functiepointer wordt geopend:

  • Functieaanwijzers kunnen alleen in een unsafe context worden gedeclareerd.
  • Methoden die een delegate* (of retourneren) delegate*gebruiken, kunnen alleen in een unsafe context worden aangeroepen.
  • De & operator voor het verkrijgen van het adres van een functie is alleen toegestaan voor static functies. (Deze regel is van toepassing op zowel lidfuncties als lokale functies).

De syntaxis heeft parallellen met het declareren van delegate typen en het gebruik van aanwijzers. Het * achtervoegsel op delegate geeft aan dat de declaratie een functieaanwijzer is. Bij & het toewijzen van een methodegroep aan een functiepointer wordt aangegeven dat de bewerking het adres van de methode gebruikt.

U kunt de oproepconventie voor een delegate* opgeven met behulp van de trefwoorden managed en unmanaged. Daarnaast kunt u voor unmanaged functie-aanwijzers de aanroepconventie opgeven. In de volgende declaraties ziet u voorbeelden van elk van deze declaraties. De eerste declaratie maakt gebruik van de managed oproepconventie. Dit is de standaardinstelling. De volgende vier gebruiken een unmanaged oproepconventie. Elk geeft een van de ECMA 335-aanroepconventies: Cdecl, Stdcall, Fastcallof Thiscall. De laatste declaratie maakt gebruik van de unmanaged oproepconventie, waarbij de CLR wordt geïnstrueerd om de standaardaanroepconventie voor het platform te kiezen. De CLR kiest de oproepconventie tijdens runtime.

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

Meer informatie over functiepointers vindt u in de functiespecificatie van de functie aanwijzer .

C#-taalspecificatie

Zie het hoofdstuk Onveilige code van de C#-taalspecificatie voor meer informatie.