Share via


Osäker kod, pekartyper och funktionspekare

Det mesta av C#-koden som du skriver är "verifierbart säker kod". Verifierbart säker kod innebär att .NET-verktyg kan kontrollera att koden är säker. I allmänhet kommer säker kod inte direkt åt minnet med hjälp av pekare. Den allokerar inte heller rådataminne. Det skapar hanterade objekt i stället.

C# stöder en unsafe kontext där du kan skriva icke-verifierad kod. I ett unsafe sammanhang kan kod använda pekare, allokera och frigöra minnesblock och anropa metoder med hjälp av funktionspekare. Osäker kod i C# är inte nödvändigtvis farlig. det är bara kod vars säkerhet inte kan verifieras.

Osäker kod har följande egenskaper:

  • Metoder, typer och kodblock kan definieras som osäkra.
  • I vissa fall kan osäker kod öka programmets prestanda genom att ta bort matrisgränskontroller.
  • Osäker kod krävs när du anropar inbyggda funktioner som kräver pekare.
  • Att använda osäker kod medför säkerhets- och stabilitetsrisker.
  • Koden som innehåller osäkra block måste kompileras med kompileringsalternativet AllowUnsafeBlocks .

Pekartyper

I ett osäkert sammanhang kan en typ vara en pekartyp, utöver en värdetyp eller en referenstyp. En deklaration av pekartyp har något av följande formulär:

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

Den typ som angavs * före i en pekartyp kallas referenstyp. Endast en ohanterad typ kan vara en referenstyp.

Pekartyper ärver inte från objektet och det finns inga konverteringar mellan pekartyper och object. Boxning och avboxning stöder inte heller pekare. Du kan dock konvertera mellan olika pekartyper och mellan pekartyper och integraltyper.

När du deklarerar flera pekare i samma deklaration skriver du asterisken (*) tillsammans med endast den underliggande typen. Den används inte som ett prefix för varje pekarnamn. Till exempel:

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

En pekare kan inte peka på en referens eller till en struct som innehåller referenser, eftersom en objektreferens kan vara skräpinsamling även om en pekare pekar på den. Skräpinsamlaren håller inte reda på om ett objekt pekas på av några pekartyper.

Värdet för pekarvariabeln av typen MyType* är adressen till en variabel av typen MyType. Följande är exempel på deklarationer av pekartyp:

  • int* p: p är en pekare till ett heltal.
  • int** p: p är en pekare till en pekare till ett heltal.
  • int*[] p: p är en endimensionell matris med pekare till heltal.
  • char* p: p är en pekare till ett tecken.
  • void* p: p är en pekare till en okänd typ.

Pekarens indirekta operator * kan användas för att komma åt innehållet på den plats som pekarvariabeln pekar på. Tänk till exempel på följande deklaration:

int* myVariable;

Uttrycket *myVariable anger variabeln int som finns på adressen i myVariable.

Det finns flera exempel på pekare i artiklarna om -instruktionenfixed. I följande exempel används nyckelordet unsafe och -instruktionen fixed och visar hur du ökar en inre pekare. Du kan klistra in den här koden i huvudfunktionen i ett konsolprogram för att köra den. De här exemplen måste kompileras med alternativuppsättningen AllowUnsafeBlocks-kompilator .

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

Du kan inte tillämpa indirektionsoperatorn på en pekare av typen void*. Du kan dock använda en gjuten för att konvertera en tomrumspekare till andra pekartyper och vice versa.

En pekare kan vara null. Om du tillämpar indirektionsoperatorn på en nullpekare uppstår ett implementeringsdefinierat beteende.

Att skicka pekare mellan metoder kan orsaka odefinierat beteende. Överväg en metod som returnerar en pekare till en lokal variabel via en in, outeller ref parameter eller som funktionsresultat. Om pekaren har angetts i ett fast block kan variabeln som den pekar på inte längre vara fast.

I följande tabell visas de operatorer och instruktioner som kan användas på pekare i en osäker kontext:

Operator/instruktion Använd
* Utför indirekt pekare.
-> Öppnar en medlem i en struct via en pekare.
[] Indexerar en pekare.
& Hämtar adressen för en variabel.
++ och -- Inkrements- och minskningspekare.
+ och - Utför pekararitmetik.
==, !=, <, >, <=, och >= Jämför pekare.
stackalloc Allokerar minne på stacken.
fixed Uttalande Korrigerar tillfälligt en variabel så att dess adress kan hittas.

Mer information om pekarrelaterade operatorer finns i Pekarrelaterade operatorer.

Alla pekartyper kan implicit konverteras till en void* typ. Alla pekartyper kan tilldelas värdet null. Alla pekartyper kan uttryckligen konverteras till andra pekartyper med hjälp av ett gjutet uttryck. Du kan också konvertera valfri integraltyp till en pekartyp eller valfri pekartyp till en integrerad typ. Dessa konverteringar kräver en explicit gjutning.

I följande exempel konverteras en int* till en byte*. Observera att pekaren pekar på variabelns lägsta adresserade byte. När du stegvis ökar resultatet, upp till storleken int (4 byte), kan du visa de återstående byteen för variabeln.

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

Buffertar med fast storlek

Du kan använda nyckelordet fixed för att skapa en buffert med en matris med fast storlek i en datastruktur. Buffertar med fast storlek är användbara när du skriver metoder som samverkar med datakällor från andra språk eller plattformar. Bufferten med fast storlek kan ta alla attribut eller modifierare som tillåts för vanliga struct-medlemmar. Den enda begränsningen är att matristypen måste vara bool, byte, char, short, int, long, , sbyte, ushort, ulonguint, , floateller double.

private fixed char name[30];

I säker kod innehåller en C#-struct som innehåller en matris inte matriselementen. Structen innehåller en referens till elementen i stället. Du kan bädda in en matris med fast storlek i en struct när den används i ett osäkert kodblock.

Storleken på följande struct beror inte på antalet element i matrisen, eftersom pathName är en referens:

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

En struct kan innehålla en inbäddad matris i osäker kod. I följande exempel har matrisen fixedBuffer en fast storlek. Du använder en fixed instruktion för att hämta en pekare till det första elementet. Du kommer åt elementen i matrisen via den här pekaren. Instruktionen fixed fäster instansfältet på fixedBuffer en specifik plats i minnet.

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

Storleken på 128-elementmatrisen char är 256 byte. Teckenbuffertar med fast storlek tar alltid 2 byte per tecken, oavsett kodning. Den här matrisstorleken är densamma även när teckenbuffertar är kopplade till API-metoder eller structs med CharSet = CharSet.Auto eller CharSet = CharSet.Ansi. Mer information finns i CharSet.

Föregående exempel visar åtkomst till fixed fält utan att fästa. En annan vanlig matris med fast storlek är bool-matrisen. Elementen i en bool matris är alltid 1 byte stora. bool matriser är inte lämpliga för att skapa bitmatriser eller buffertar.

Buffertar med fast storlek kompileras med System.Runtime.CompilerServices.UnsafeValueTypeAttribute, som instruerar CLR (Common Language Runtime) att en typ innehåller en ohanterad matris som potentiellt kan flöda över. Minne som allokeras med stackalloc aktiverar också automatiskt funktioner för identifiering av buffertöverkörning i CLR. I föregående exempel visas hur en buffert med fast storlek kan finnas i en unsafe struct.

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

Den kompilatorgenererade C#-filen för Buffer tillskrivs på följande sätt:

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

Buffertar med fast storlek skiljer sig från vanliga matriser på följande sätt:

  • Får endast användas i en unsafe kontext.
  • Kan bara vara instansfält för structs.
  • De är alltid vektorer eller endimensionella matriser.
  • Deklarationen bör innehålla längden, till exempel fixed char id[8]. Du kan inte använda fixed char id[].

Så här använder du pekare för att kopiera en matris med byte

I följande exempel används pekare för att kopiera byte från en matris till en annan.

I det här exemplet används nyckelordet osäkert , vilket gör att du kan använda pekare i Copy metoden. Den fasta instruktionen används för att deklarera pekare till käll- och målmatriserna. Instruktionen fixed fäster platsen för käll- och målmatriserna i minnet så att de inte flyttas av skräpinsamling. Minnesblocken för matriserna är inte fästa när fixed blocket har slutförts. Copy Eftersom metoden i det här exemplet använder nyckelordet unsafe måste den kompileras med kompileringsalternativet AllowUnsafeBlocks.

Det här exemplet kommer åt elementen i båda matriserna med hjälp av index i stället för en andra ohanterad pekare. Deklarationen pSource av pekarna och pTarget fäster matriserna.

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

Funktionspekare

C# innehåller delegate typer för att definiera objekt för säker funktionspekare. Att anropa ett ombud innebär att instansiera en typ som härletts från System.Delegate och göra ett virtuellt metodanrop till dess Invoke metod. Det här virtuella anropet använder IL-instruktionen callvirt . I prestandakritiska kodsökvägar är det effektivare att använda IL-instruktionen calli .

Du kan definiera en funktionspekare med hjälp av syntaxen delegate* . Kompilatorn anropar funktionen med hjälp av instruktionen calli i stället för att instansiera ett delegate objekt och anropa Invoke. Följande kod deklarerar två metoder som använder en delegate eller en delegate* för att kombinera två objekt av samma typ. Den första metoden använder en System.Func<T1,T2,TResult> ombudstyp. Den andra metoden använder en delegate* deklaration med samma parametrar och returtyp:

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

Följande kod visar hur du deklarerar en statisk lokal funktion och anropar metoden med hjälp av en pekare till den UnsafeCombine lokala funktionen:

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

Föregående kod illustrerar flera av reglerna för funktionen som används som en funktionspekare:

  • Funktionspekare kan bara deklareras i en unsafe kontext.
  • Metoder som tar en delegate* (eller returnerar en delegate*) kan bara anropas i en unsafe kontext.
  • Operatorn & för att hämta adressen till en funktion tillåts endast för static funktioner. (Den här regeln gäller både medlemsfunktioner och lokala funktioner).

Syntaxen har paralleller med att delegate deklarera typer och använda pekare. Suffixet *delegate anger att deklarationen är en funktionspekare. När & du tilldelar en metodgrupp till en funktionspekare anger att åtgärden tar metodens adress.

Du kan ange anropskonventionen för en delegate* med hjälp av nyckelorden managed och unmanaged. För funktionspekare kan du dessutom unmanaged ange anropskonventionen. Följande deklarationer visar exempel på var och en. Den första deklarationen använder anropskonventionen managed , som är standard. De kommande fyra använder en anropskonvention unmanaged . Var och en anger någon av ECMA 335-anropskonventionerna: Cdecl, Stdcall, Fastcalleller Thiscall. Den senaste deklarationen använder anropskonventionen unmanaged och instruerar CLR att välja standardanropskonventionen för plattformen. CLR väljer anropskonventionen vid körning.

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

Du kan lära dig mer om funktionspekare i funktionspekarens funktionsspecifikation.

Språkspecifikation för C#

Mer information finns i kapitlet Osäker kod i C#-språkspecifikationen.