Unsicherer Code, Zeigertypen und Funktionszeiger
Der großteil des von Ihnen geschriebenen C#-Codes ist "sicherer Code". Sicheren Code bedeutet, dass .NET-Tools überprüfen können, ob der Code sicher ist. Im Allgemeinen greift der sichere Code nicht direkt mithilfe von Zeigern auf den Arbeitsspeicher zu. Außerdem ordnet er keinen unformatierten Arbeitsspeicher zu. Stattdessen werden verwaltete Objekte erstellt.
C# unterstützt einen unsafe
Kontext, in dem Sie unverifizierbaren Code schreiben können. In einem unsafe
Kontext kann Code Zeiger verwenden, Speicherblöcke zuordnen und freigeben sowie Methoden mithilfe von Funktionszeigern aufrufen. Unsicherer Code in C# ist nicht unbedingt gefährlich; Es ist nur Code, dessen Sicherheit nicht überprüft werden kann.
Unsicherer Code hat die folgenden Eigenschaften:
- Methoden, Typen und Codeblöcke können als unsicher definiert werden.
- In einigen Fällen kann unsicherer Code die Leistung einer Anwendung erhöhen, indem Array-Begrenzungsprüfungen entfernt werden.
- Unsicherer Code ist erforderlich, wenn Sie systemeigene Funktionen aufrufen, die Zeiger erfordern.
- Die Verwendung unsicherer Code führt zu Sicherheits- und Stabilitätsrisiken.
- Der Code, der unsichere Blöcke enthält, muss mit der AllowUnsafeBlocks Compileroption kompiliert werden.
Zeigertypen
In einem unsicheren Kontext kann es sich bei einem Typ um einen Zeigertyp, zusätzlich zu einem Werttyp oder um einen Verweistyp. Eine Zeigertypdeklaration akzeptiert eine der folgenden Formen:
type* identifier;
void* identifier; //allowed but not recommended
Der typ, der vor dem *
in einem Zeigertyp angegeben wird, wird als Referenztypbezeichnet.
Zeigertypen erben nicht von Objekt und es existieren keine Umwandlungen zwischen Zeigertypen und object
. Weiterhin unterstützen Boxing und Unboxing keine Zeiger. Sie können jedoch zwischen verschiedenen Zeigertypen und zwischen Zeigertypen und integralen Typen konvertieren.
Wenn Sie mehrere Zeiger in derselben Deklaration deklarieren, schreiben Sie das Sternchen (*
) zusammen mit dem zugrunde liegenden Typ. Sie wird nicht als Präfix für jeden Zeigernamen verwendet. Zum Beispiel:
int* p1, p2, p3; // Ok
int *p1, *p2, *p3; // Invalid in C#
In der Garbage Collection wird nicht nachgehalten, ob von einem der Zeigertypen auf ein Objekt verwiesen wird. Wenn es sich bei dem Verweiser um ein Objekt im verwalteten Heap handelt (einschließlich lokaler Variablen, die von Lambda-Ausdrücken oder anonymen Delegaten erfasst werden), muss das Objekt angeheftet werden, solange der Zeiger verwendet wird.
Der Wert der Zeigervariable vom Typ MyType*
ist die Adresse einer Variablen vom Typ MyType
. Im Folgenden sind Beispiele für Zeigertypdeklarationen aufgeführt:
int* p
:p
ist ein Zeiger auf eine ganze Zahl.-
int** p
:p
ist ein Zeiger auf einen Zeiger auf einen ganzzahligen Wert. int*[] p
:p
ist ein eindimensionales Array von Zeigern auf ganze Zahlen.-
char* p
:p
ist ein Zeiger auf eine char-Variable. void* p
:p
ist ein Zeiger auf einen unbekannten Typ.
Der Zeigerdereferenzierungsoperator *
kann verwendet werden, um auf den Inhalt an der Position zuzugreifen, auf die die Zeigervariable verweist. Betrachten Sie beispielsweise die folgende Deklaration:
int* myVariable;
Der Ausdruck *myVariable
bezeichnet die int
Variable, die in der Adresse myVariable
gespeichert ist.
Es gibt mehrere Beispiele für Zeiger in den Artikeln zur fixed
-Anweisung. Im folgenden Beispiel wird das Schlüsselwort unsafe
und die fixed
-Anweisung verwendet, und es wird gezeigt, wie ein Innenzeiger inkrementiert wird. Sie können diesen Code in die Hauptfunktion einer Konsolenanwendung einfügen, um ihn auszuführen. Diese Beispiele müssen mit dem AllowUnsafeBlocks Compileroptionssatz 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
*/
Sie können den Dereferenzierungsoperator nicht auf einen Zeiger vom Typ void*
anwenden. Sie können jedoch eine Umwandlung verwenden, um einen void-Zeiger in einen anderen Zeigertyp und umgekehrt zu konvertieren.
Ein Zeiger kann null
sein. Das Anwenden des Dereferenzierungsoperators auf einen Nullzeiger bewirkt ein implementierungsdefiniertes Verhalten.
Das Übergeben von Zeigern zwischen Methoden kann zu einem nicht definierten Verhalten führen. Erwägen Sie eine Methode, die einen Zeiger auf eine lokale Variable über eine in
, out
oder ref
Parameter oder als Funktionsergebnis zurückgibt. Wenn der Zeiger in einem festen Block gesetzt wurde, ist die Variable, auf die er zeigt, möglicherweise nicht mehr fest.
In der folgenden Tabelle sind die Operatoren und Anweisungen aufgeführt, die auf Zeigern in einem unsicheren Kontext ausgeführt werden können:
Operator/Anweisung | Zweck |
---|---|
* |
Führt eine Zeigerdereferenzierung aus. |
-> |
Greift über einen Zeiger auf ein Element 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 miteinander. |
stackalloc |
Belegt Speicher für den Stapel. |
fixed -Anweisung |
Sperrt vorübergehend eine Variable, damit ihre Adresse ermittelt werden kann. |
Weitere Informationen zu zeigerbezogenen Operatoren finden Sie unter Zeiger-Operatoren.
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 einen beliebigen Zeigertyp in einen integralen Typ konvertieren. Diese Konvertierungen erfordern eine explizite Umwandlung.
Im folgenden Beispiel wird ein int*
in eine byte*
konvertiert. Beachten Sie, dass der Zeiger auf das niedrigste adressierte Byte der Variablen zeigt. Wenn Sie das Ergebnis nacheinander erhöhen, können Sie bis zur Größe von int
(4 Byte) die verbleibenden Bytes der Variablen 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 schlüsselwort fixed
verwenden, um einen Puffer mit einem Array mit 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 alle Attribute oder Modifizierer verwenden, die für reguläre Strukturmitglieder zulässig sind. Die einzige Einschränkung ist, dass der Arraytyp bool
, byte
, char
, short
, int
, long
, sbyte
, ushort
, uint
, ulong
, float
oder double
sein muss.
private fixed char name[30];
Im sicheren Code enthält eine C#-Struktur, die ein Array enthält, nicht die Arrayelemente. Die Struktur enthält stattdessen einen Verweis auf die Elemente. Sie können ein Array mit fester Größe in einer -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 unsicherem Code enthalten. Im folgenden Beispiel weist das fixedBuffer
Array eine feste Größe auf. Sie verwenden eine fixed
-Anweisung, um einen Zeiger auf das erste Element abzurufen. Sie greifen über diesen Zeiger auf die Elemente des Arrays zu. Mit der fixed
-Anweisung wird das fixedBuffer
-Instanzfeld an einen bestimmten Speicherort im Arbeitsspeicher angeheftet.
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-Elements char
Arrays beträgt 256 Byte. char-Puffer mit fester Größe verwenden unabhängig von der Codierung immer 2 Bytes pro Zeichen. Diese Arraygröße bleibt unverändert, auch wenn Zeichenpuffer an API-Methoden oder -Strukturen mit CharSet = CharSet.Auto
oder CharSet = CharSet.Ansi
übergeben werden. Weitere Informationen finden Sie unter CharSet.
Im obigen Beispiel wird der Zugriff auf fixed
-Felder ohne Fixieren veranschaulicht. Ein weiteres gängiges 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 dem System.Runtime.CompilerServices.UnsafeValueTypeAttributekompiliert, wodurch die Common Language Runtime (CLR) angewiesen wird, dass ein Typ ein nicht verwaltetes Array enthält, das potenziell überlaufen kann. Der mit stackalloc zugewiesene Speicher aktiviert automatisch auch Funktionen zur Erkennung von Pufferüberläufen in der CLR. Das vorherige Beispiel zeigt, wie ein Puffer mit fester Größe in einem unsafe struct
vorhanden sein könnte.
internal unsafe struct Buffer
{
public fixed char fixedBuffer[128];
}
Das vom Compiler generierte C# für Buffer
wird wie folgt zugeordnet:
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 von regulären Arrays auf folgende Weise:
- Kann 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]
. Sie könnenfixed char id[]
nicht verwenden.
Wie man Zeiger verwendet, um ein Byte-Array zu kopieren
Im folgenden Beispiel werden Zeiger verwendet, um Bytes aus einem Array in ein anderes zu kopieren.
In diesem Beispiel wird das unsichere Schlüsselwort 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 die Speicherbereinigung keine Arrays verschiebt. 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 AllowUnsafeBlocks Compileroption kompiliert werden.
In diesem Beispiel wird anstelle eines zweiten nicht verwalteten Zeigers unter Verwendung von Indizes auf die Elemente beider Arrays zugegriffen. 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 zum Definieren sicherer Funktionszeigerobjekte bereit. 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. Bei leistungskritischen Codepfaden ist die Verwendung der calli
IL-Anweisung effizienter.
Sie können einen Funktionszeiger mithilfe der delegate*
Syntax definieren. Der Compiler ruft die Funktion mithilfe der calli
Anweisung auf, anstatt ein delegate
-Objekt zu instanziieren und Invoke
aufzurufen. Der folgende Code deklariert zwei Methoden, die eine delegate
oder eine delegate*
verwenden, um zwei Objekte desselben Typs zu kombinieren. Die erste Methode verwendet einen Delegattyp vom Typ System.Func<T1,T2,TResult>. Die zweite Methode verwendet eine delegate*
-Deklaration mit denselben Parametern und rückgabetyp:
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);
Der folgende Code zeigt, wie Sie eine statische lokale Funktion deklarieren und die UnsafeCombine
-Methode mithilfe eines Zeigers auf diese lokale Funktion aufrufen:
int product = 0;
unsafe
{
static int localMultiply(int x, int y) => x * y;
product = UnsafeCombine(&localMultiply, 3, 4);
}
Der vorangehende Code veranschaulicht mehrere der Regeln für die Funktion, auf die als Funktionszeiger zugegriffen wird:
- Funktionszeiger können nur in einem
unsafe
Kontext deklariert werden. - Methoden, die eine
delegate*
verwenden (oder einedelegate*
zurückgeben), können nur in einemunsafe
Kontext aufgerufen werden. - Der
&
Operator zum Abrufen der Adresse einer Funktion ist nur fürstatic
Funktionen zulässig. (Diese Regel gilt sowohl für Memberfunktionen als auch für lokale Funktionen.)
Die Syntax weist Parallelen zum Deklarieren von delegate
-Typen und zur Verwendung von Zeigern auf. Das *
Suffix für delegate
gibt an, dass die Deklaration ein Funktionszeigerist. Die &
beim Zuweisen einer Methodengruppe zu einem Funktionszeiger gibt an, dass der Vorgang die Adresse der Methode verwendet.
Sie können die Anrufkonvention für eine delegate*
mithilfe der Schlüsselwörter managed
und unmanaged
angeben. Darüber hinaus können Sie für unmanaged
Funktionszeiger die aufrufende Konvention angeben. Die folgenden Deklarationen zeigen Beispiele für jede. Die erste Deklaration verwendet die managed
Aufrufkonvention, die standard ist. Die nächsten vier verwenden die unmanaged
-Aufrufkonvention. Jede gibt eine der ECMA 335-Anrufkonventionen an: Cdecl
, Stdcall
, Fastcall
oder Thiscall
. Die letzte Deklaration verwendet die unmanaged
Aufrufkonvention und weist die CLR an, die Standardanrufkonvention für die Plattform zu wählen. Die CLR wählt die Aufrufkonvention zur Laufzeit aus.
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);
Weitere Informationen zu Funktionszeigern finden Sie in derFunktionszeiger-Featurespezifikation.
C#-Sprachspezifikation
Weitere Informationen finden Sie im Kapitel Unsicherer Code in der C#-Sprachspezifikation.