Unsicherer Code, Zeigertypen und Funktionszeiger

Der größte Teil des C#-Codes, den Sie schreiben, ist „überprüfbar sicherer Code“. Überprüfbar sicherer Code bedeutet, dass .NET-Tools sicherstellen können, dass der Code sicher ist. Üblicherweise greift sicherer Code nicht direkt über Zeiger auf den Arbeitsspeicher zu. Außerdem ordnet er keinen unformatierten Arbeitsspeicher zu. Stattdessen erstellt er verwaltete Objekte.

C# unterstützt einen unsafe-Kontext, in dem Sie nicht überprüfbaren Code schreiben können. In einem unsafe-Kontext kann Code Zeiger verwenden, Speicherblöcke zuordnen und freigeben sowie Methoden mit Funktionszeigern aufrufen. Unsicherer Code in C# ist nicht notwendigerweise gefährlich. Es handelt sich dabei lediglich um Code, dessen Sicherheit nicht überprüft werden kann.

Unsicherer Code verfügt über die folgenden Eigenschaften:

  • Methoden, Typen und Codeblöcke können als unsicher definiert werden.
  • In manchen Fällen kann unsicherer Code die Leistung einer Anwendung erhöhen, indem die Überprüfung von Arraygrenzen entfernt wird.
  • Unsicherer Code ist erforderlich, wenn Sie native Funktionen aufrufen, die Zeiger erfordern.
  • Die Verwendung von unsicherem Code führt zu Sicherheits- und Stabilitätsrisiken.
  • Code, in dem unsichere Blöcke enthalten sind, muss mit der Compileroption AllowUnsafeBlocks kompiliert werden.

Zeigertypen

In einem unsicheren Kontext kann ein Typ ein Zeigertyp, zusätzlich zu einem Werttyp, oder ein Verweistyp sein. Eine Zeigertypdeklaration erfolgt in einer der folgenden Formen:

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

Der Typ, der vor * in einem Zeigertyp angegeben wird, wird als Verweistyp bezeichnet. Nur ein nicht verwalteter Typ kann ein Verweistyp sein.

Zeigertypen erben nicht von object, und es ist keine Konvertierung zwischen Zeigertypen und object möglich. Weiterhin unterstützen Boxing und Unboxing keine Zeiger. Es ist jedoch möglich, Konvertierungen zwischen verschiedenen Zeigertypen sowie zwischen Zeigertypen und ganzzahligen Typen durchzuführen.

Wenn Sie mehrere Zeiger in derselben Deklaration deklarieren, schreiben Sie das Sternchen (*) nur zusammen mit dem zugrunde liegenden Typ. Er wird nicht als Präfix für jeden Zeigernamen verwendet. Beispiel:

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

Ein Zeiger kann nicht auf einen Verweis oder eine Struktur verweisen, der oder die Verweise enthält, da ein Objektverweis auch dann in die Garbage Collection aufgenommen werden kann, wenn ein Zeiger darauf verweist. In der Garbage Collection wird nicht nachgehalten, ob von einem der Zeigertypen auf ein Objekt verwiesen wird.

Der Wert der Zeigervariablen vom Typ MyType* ist die Adresse einer Variablen vom Typ MyType. Im Folgenden finden Sie Beispiele für Zeigertypdeklarationen:

  • int* p: p ist ein Zeiger auf einen ganzzahligen Wert.
  • int** p: p ist ein Zeiger auf einen Zeiger auf einen ganzzahligen Wert.
  • int*[] p: p ist ein eindimensionales Array von Zeigern auf ganzzahlige Werte.
  • char* p: p ist ein Zeiger auf eine char-Variable.
  • void* p: p ist ein Zeiger auf einen unbekannten Typ.

Sie können den Zeigerdereferenzierungsoperator * verwenden, um auf den Inhalt an der Speicherstelle zuzugreifen, auf die die Zeigervariable zeigt. Betrachten Sie beispielsweise die folgende Deklaration:

int* myVariable;

Der Ausdruck *myVariable kennzeichnet die int-Variable, die sich an der in myVariable enthaltenen Adresse befindet.

Es gibt mehrere Beispiele für Zeiger in den Artikeln zur fixed-Anweisung. Im folgenden Beispiel wird die Verwendung des unsafe-Schlüsselworts und der fixed-Anweisung sowie die Vorgehensweise zum Erhöhen eines inneren Zeigers veranschaulicht. Sie können diesen Code in die Hauptmethode einer Konsolenanwendung einfügen, um ihn auszuführen. Diese Beispiele müssen mithilfe der Compileroption AllowUnsafeBlocks kompiliert werden.

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

Der Dereferenzierungsoperator kann nicht auf Zeiger vom Typ void* angewendet werden. Sie können jedoch eine Umwandlung verwenden, um einen void-Zeiger in einen anderen Zeigertyp und umgekehrt zu konvertieren.

Ein Zeiger kann den Wert null annehmen. Die Anwendung des Dereferenzierungsoperators auf einen NULL-Zeiger führt zu einem in der Implementierung definierten Verhalten.

Die Übergabe von Zeigern zwischen Methoden kann zu nicht definiertem Verhalten führen. Ziehen Sie eine Methode in Betracht, die einen Zeiger auf eine lokale Variable als einen in-, out- oder ref-Parameter oder als Funktionsergebnis zurückgibt. Wenn der Zeiger in einem fixed-Block festgelegt wurde, ist die Variable, auf die der Zeiger verweist, möglicherweise nicht fixiert.

In der folgenden Tabelle werden die Operatoren und Anweisungen aufgelistet, die in einem unsicheren Kontext auf Zeiger angewendet werden können.

Operator/Anweisung Verwendung
* Führt eine Zeigerdereferenzierung aus.
-> Greift über einen Zeiger auf einen Member einer Struktur zu.
[] Indiziert einen Zeiger.
& Ruft die Adresse einer Variablen ab.
++ und -- Inkrementiert und dekrementiert Zeiger.
+ und - Führt Zeigerarithmetik aus.
==, !=, <, >, <= und >= Vergleicht Zeiger.
stackalloc Belegt Speicher für den Stapel.
fixed-Anweisung Fixiert eine Variable vorübergehend, damit ihre Adresse gefunden werden kann.

Weitere Informationen zu zeigerbezogenen Operatoren finden Sie unter Operatoren im Zusammenhang mit Zeigern.

Jeder Zeigertyp kann implizit in einen void*-Typ konvertiert werden. Jedem Zeigertyp kann der Wert null zugewiesen werden. Jeder Zeigertyp kann mithilfe eines Umwandlungsausdrucks explizit in einen anderen Zeigertyp konvertiert werden. Sie können auch jeden integralen Typ in einen Zeigertyp oder jeden Zeigertyp in einen integralen Typ konvertieren. Diese Konvertierungen erfordern eine explizite Umwandlung.

Im folgenden Beispiel wird ein int*-Typ in einen byte*-Typ konvertiert. Beachten Sie, dass der Zeiger auf das Byte der Variable mit der niedrigsten Adresse zeigt. Wenn Sie das Ergebnis nach und nach bis auf die Größe von int (4 Bytes) erhöhen, können Sie die verbleibenden Bytes der Variable anzeigen.

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

Puffer fester Größe

Sie können das fixed-Schlüsselwort verwenden, um einen Puffer mit einem Array fester Größe in einer Datenstruktur zu erstellen. Puffer mit fester Größe sind nützlich, wenn Sie Methoden schreiben, die mit Datenquellen aus anderen Sprachen oder Plattformen zusammenarbeiten. Der Puffer mit fester Größe kann sämtliche Attribute und Modifizierer verwenden, die für reguläre Strukturmember zulässig sind. Die einzige Einschränkung besteht darin, dass der Arraytyp bool, byte, char, short, int, long, sbyte, ushort, uint, ulong, float oder double sein muss.

private fixed char name[30];

Eine C#-Struktur in sicherem Code, die ein Array enthält, enthält nicht die Elemente des Arrays. Stattdessen enthält die Struktur einen Verweis auf die Elemente. Sie können ein Array mit einer festen Größe in eine Struktur einbetten, wenn es in einem unsicheren Codeblock verwendet wird.

Die Größe der folgenden struct hängt nicht von der Anzahl der Elemente im Array ab, da pathName ein Verweis ist:

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

Eine Struktur kann ein eingebettetes Array in unsicheren Code enthalten. Im folgenden Beispiel verfügt das fixedBuffer-Array über eine feste Größe. Sie können eine fixed-Anweisung verwenden, um einen Zeiger auf das erste Element festzulegen. Über diesen Zeiger können Sie auf die Elemente des Arrays zugreifen. Die fixed-Anweisung fixiert das Instanzenfeld fixedBuffer an einem bestimmten Speicherort im Arbeitsspeicher.

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

Die Größe des 128-Element-char-Arrays beträgt 256 Bytes. char-Puffer mit fester Größe verwenden unabhängig von der Codierung immer 2 Bytes pro Zeichen. Diese Arraygröße ist selbst dann identisch, wenn char-Puffer mit CharSet = CharSet.Auto oder CharSet = CharSet.Ansi zu API-Methoden oder Strukturen gemarshallt werden. Weitere Informationen finden Sie unter CharSet.

Im obigen Beispiel wird der Zugriff auf fixed-Felder ohne Fixieren veranschaulicht. Ein anderes häufiges Array mit fester Größe ist das bool-Array. Die Elemente in einem bool-Array sind immer 1 Byte groß. bool-Arrays eignen sich nicht zum Erstellen von Bitarrays oder Puffern.

Puffer mit fester Größe werden mit der System.Runtime.CompilerServices.UnsafeValueTypeAttribute-Klasse kompiliert, die die Common Language Runtime (CLR) anweist, dass ein Typ ein nicht verwaltetes Array enthält, das potenziell überlaufen kann. Arbeitsspeicher, der mit stackalloc zugeordnet wurde, ermöglicht auch automatisch Funktionen zur Erkennung von Pufferüberlauf in der CLR. Im obigen Beispiel wird gezeigt, wie ein Puffer mit fester Größe in einer unsicheren Struktur (unsafe struct) vorhanden sein kann.

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

Der vom Compiler für Buffer generierte C#-Code wird wie folgt attributiert:

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

Puffer mit fester Größe unterscheiden sich folgendermaßen von normalen Arrays:

  • Können nur in einem unsafe Kontext verwendet werden
  • Können nur Instanzfelder von Strukturen sein
  • Sie sind immer Vektoren oder eindimensionale Arrays.
  • Die Deklaration sollte die Länge enthalten, z. B. fixed char id[8]. Es ist nicht möglich, fixed char id[] zu verwenden.

Verwenden von Zeigern zum Kopieren eines Bytearrays

In folgendem Beispiel werden Zeiger verwendet, um Bytes aus einem Array in ein anderes zu kopieren.

In diesem Beispiel wird das Schlüsselwort unsafe verwendet, mit dem Sie Zeiger in der Copy-Methode verwenden können. Die Anweisung fixed wird verwendet, um Zeiger auf das Quell- und Zielarray zu deklarieren. Diese fixed-Anweisung heftet den Speicherort des Quell- und Zielarrays im Speicher an, damit diese während der automatischen Speicherbereinigung nicht verschoben werden. Die Speicherblöcke der Arrays werden gelöst, wenn der fixed-Block abgeschlossen wird. Da die Copy-Methode in diesem Beispiel das Schlüsselwort unsafe verwendet, muss sie mit der Compileroption AllowUnsafeBlocks kompiliert werden.

In diesem Beispiel werden Indizes anstelle eines zweiten nicht verwalteten Zeigers verwendet, um auf die Elemente beider Arrays zuzugreifen. Die Deklaration der Zeiger pSource und pTarget heftet die Arrays an.

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

Funktionszeiger

C# stellt delegate-Typen bereit, um sichere Funktionszeigerobjekte zu definieren. Das Aufrufen eines Delegaten umfasst das Instanziieren eines von System.Delegate abgeleiteten Typs und das Ausführen eines virtuellen Methodenaufrufs für dessen Invoke-Methode. Dieser virtuelle Aufruf verwendet die callvirt-IL-Anweisung. In leistungskritischen Codepfaden ist die Verwendung der calli-IL-Anweisung effizienter.

Sie können einen Funktionszeiger mit der delegate*-Syntax definieren. Der Compiler ruft die Funktion mit der calli-Anweisung auf, anstatt ein delegate-Objekt zu instanziieren und Invoke aufzurufen. Der folgende Code deklariert zwei Methoden, in denen ein delegate- oder ein delegate*-Zeiger verwendet wird, um zwei Objekte desselben Typs zu kombinieren. In der ersten Methode wird ein System.Func<T1,T2,TResult>-Delegattyp verwendet. In der zweiten Methode wird eine delegate*-Deklaration mit denselben Parametern und demselben Rückgabetyp verwendet:

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

Der folgende Code zeigt, wie Sie eine statische lokale Funktion deklarieren und die UnsafeCombine-Methode mithilfe eines Zeigers auf diese lokale Funktion aufrufen:

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

Der vorangehende Code veranschaulicht einige der Regeln für die Funktion, auf die als Funktionszeiger zugegriffen wird:

  • Funktionszeiger können nur in einem unsafe-Kontext deklariert werden.
  • Methoden, die einen delegate*-Zeiger verwenden (oder einen delegate*-Zeiger zurückgeben), können nur in einem unsafe-Kontext aufgerufen werden.
  • Der &-Operator zum Abrufen der Adresse einer Funktion ist nur für static-Funktionen zulässig. (Diese Regel gilt sowohl für Memberfunktionen als auch für lokale Funktionen.)

Die Syntax weist Parallelen mit dem Deklarieren von delegate-Typen und dem Verwenden von Zeigern auf. Das *-Suffix bei delegate kennzeichnet, dass die Deklaration ein Funktionszeiger ist. Das & kennzeichnet, wenn eine Methodengruppe einem Funktionszeiger zugewiesen wird, dass der Vorgang die Adresse der Methode verwendet.

Sie können die Aufrufkonvention für einen delegate*-Zeiger mit den Schlüsselwörtern managed und unmanaged angeben. Außerdem können Sie für unmanaged-Funktionszeiger die Aufrufkonvention angeben. Die folgenden Deklarationen zeigen Beispiele für jedes Schlüsselwort. In der ersten Deklaration wird die managed-Aufrufkonvention verwendet, die die Standardkonvention ist. In den nächsten vier wird eine unmanaged-Aufrufkonvention verwendet. In jeder Deklaration ist eine der Ecma International 335-Aufrufkonventionen angegeben: Cdecl, Stdcall, Fastcall oder Thiscall. In der letzten Deklaration wird die unmanaged-Aufrufkonvention verwendet, wodurch die CLR angewiesen wird, die Standardaufrufkonvention für die Plattform auszuwählen. Die CLR wählt die Aufrufkonvention zur Laufzeit aus.

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

Weitere Informationen zu Funktionszeigern finden Sie in derFunktionszeiger-Featurespezifikation.

C#-Sprachspezifikation

Weitere Informationen finden Sie im Kapitel Unsicherer Code in der C#-Sprachspezifikation.