Niebezpieczny kod, typy wskaźników i wskaźniki funkcji
Większość pisanego kodu w języku C# to "weryfikowalny bezpieczny kod". Weryfikowalny bezpieczny kod oznacza, że narzędzia platformy .NET mogą sprawdzić, czy kod jest bezpieczny. Ogólnie rzecz biorąc, bezpieczny kod nie uzyskuje bezpośredniego dostępu do pamięci przy użyciu wskaźników. Nie przydziela również nieprzetworzonej pamięci. Zamiast tego tworzy zarządzane obiekty.
Język C# obsługuje unsafe
kontekst, w którym można napisać nieweryfikowalny kod. unsafe
W kontekście kod może używać wskaźników, przydzielać i zwalniać bloki pamięci oraz wywoływać metody przy użyciu wskaźników funkcji. Niebezpieczny kod w języku C# nie musi być niebezpieczny; to po prostu kod, którego nie można zweryfikować bezpieczeństwa.
Niebezpieczny kod ma następujące właściwości:
- Metody, typy i bloki kodu można zdefiniować jako niebezpieczne.
- W niektórych przypadkach niebezpieczny kod może zwiększyć wydajność aplikacji, usuwając kontrole granic tablicy.
- Niebezpieczny kod jest wymagany podczas wywoływania funkcji natywnych, które wymagają wskaźników.
- Użycie niebezpiecznego kodu wprowadza zagrożenia bezpieczeństwa i stabilności.
- Kod zawierający niebezpieczne bloki należy skompilować za pomocą opcji kompilatora AllowUnsafeBlocks .
Typy wskaźnika
W niebezpiecznym kontekście typ może być typem wskaźnika, oprócz typu wartości lub typu odwołania. Deklaracja typu wskaźnika ma jedną z następujących form:
type* identifier;
void* identifier; //allowed but not recommended
Typ określony przed *
typem wskaźnika jest nazywany typem odwołania. Tylko niezarządzany typ może być typem odwołania.
Typy wskaźników nie dziedziczą z obiektu i nie istnieją konwersje między typami wskaźników i object
. Ponadto boxing i unboxing nie obsługują wskaźników. Można jednak wykonywać konwersje między różnymi typami wskaźnika oraz między typami wskaźnika a typami całkowitymi.
Podczas deklarowania wielu wskaźników w tej samej deklaracji zapisuje się gwiazdkę (*
) razem tylko z typem bazowym. Nie jest on używany jako prefiks do każdej nazwy wskaźnika. Na przykład:
int* p1, p2, p3; // Ok
int *p1, *p2, *p3; // Invalid in C#
Wskaźnik nie może wskazywać odwołania lub do struktury zawierającej odwołania, ponieważ odwołanie do obiektu może być bezużyteczne, nawet jeśli wskaźnik wskazuje na niego. Moduł odśmiecający śmieci nie śledzi, czy obiekt jest wskazywany przez jakiekolwiek typy wskaźników.
Wartość zmiennej wskaźnika typu MyType*
to adres zmiennej typu MyType
. Poniżej przedstawiono przykłady deklaracji typów wskaźnika:
int* p
:p
jest wskaźnikiem do liczby całkowitej.int** p
:p
jest wskaźnikiem wskaźnika do liczby całkowitej.int*[] p
:p
jest jednowymiarową tablicą wskaźników do liczb całkowitych.char* p
:p
jest wskaźnikiem do znaku.void* p
:p
jest wskaźnikiem do nieznanego typu.
Operator *
pośredni wskaźnika może służyć do uzyskiwania dostępu do zawartości w lokalizacji wskazanej przez zmienną wskaźnika. Na przykład przeanalizujmy następującą deklarację:
int* myVariable;
Wyrażenie *myVariable
oznacza zmienną int
znalezioną pod adresem zawartym w myVariable
elem.
W artykułach dotyczących instrukcji fixed
znajduje się kilka przykładów wskaźników. W poniższym przykładzie użyto słowa kluczowego unsafe
i instrukcji fixed
oraz pokazano, jak zwiększać wskaźnik wewnętrzny. Ten kod można wkleić do funkcji Main aplikacji konsoli, aby go uruchomić. Te przykłady należy skompilować przy użyciu zestawu opcji kompilatora AllowUnsafeBlocks .
// 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
*/
Nie można zastosować operatora pośredniego do wskaźnika typu void*
. Można jednak użyć rzutowania, aby przekonwertować wskaźnik typu void na wskaźnik dowolnego innego typu i odwrotnie.
Wskaźnik może mieć wartość null
. Zastosowanie operatora pośredniego do wskaźnika o wartości null powoduje użycie zachowania zdefiniowanego w implementacji.
Przekazywanie wskaźników między metodami może powodować niezdefiniowane zachowanie. Rozważ metodę zwracającą wskaźnik do zmiennej lokalnej za pomocą parametru in
, out
lub ref
jako wyniku funkcji. Jeśli wskaźnik został ustawiony w stałym bloku, wskazywana przez niego zmienna może już nie być stała.
W poniższej tabeli wymieniono operatory i instrukcje, które mogą wykonywać operacje na wskaźnikach w kontekście słowa kluczowego „unsafe”:
Operator/instrukcja | Używanie |
---|---|
* |
Wykonuje operację wskaźnika pośredniego. |
-> |
Uzyskuje dostęp do elementu członkowskiego struktury za pomocą wskaźnika. |
[] |
Indeksuje wskaźnik. |
& |
Uzyskuje adres zmiennej. |
++ i -- |
Zwiększa i zmniejsza wartość wskaźnika. |
+ i - |
Wykonuje operacje arytmetyczne na wskaźniku. |
== , != , , < , > , <= i >= |
Porównuje wskaźniki. |
stackalloc |
Przydziela pamięć na stosie. |
fixed Instrukcja |
Tymczasowo ustala zmienną, dzięki czemu można znaleźć ten adres. |
Aby uzyskać więcej informacji na temat operatorów związanych ze wskaźnikiem, zobacz Operatory związane ze wskaźnikiem.
Dowolny typ wskaźnika można niejawnie przekonwertować na void*
typ. Do dowolnego typu wskaźnika można przypisać wartość null
. Dowolny typ wskaźnika można jawnie przekonwertować na dowolny inny typ wskaźnika przy użyciu wyrażenia rzutowania. Można również przekonwertować dowolny typ całkowity na typ wskaźnika lub dowolny typ wskaźnika na typ całkowity. Konwersje te wymagają jawnego rzutu.
Poniższy przykład konwertuje element int*
na .byte*
Zwróć uwagę, że wskaźnik wskazuje najniższy adresowany bajt zmiennej. Po kolejnych przyrostach wyniku do rozmiaru int
(4 bajtów) można wyświetlić pozostałe bajty zmiennej.
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
*/
}
Bufory o stałym rozmiarze
Słowo kluczowe umożliwia fixed
utworzenie buforu z tablicą o stałym rozmiarze w strukturze danych. Bufory o stałym rozmiarze są przydatne podczas pisania metod, które współdziałają ze źródłami danych z innych języków lub platform. Bufor o stałym rozmiarze może przyjmować dowolne atrybuty lub modyfikatory dozwolone dla zwykłych składowych struktury. Jedynym ograniczeniem jest to, że typ tablicy musi być bool
, , int
short
long
sbyte
char
byte
uint
ulong
ushort
float
lub .double
private fixed char name[30];
W bezpiecznym kodzie struktura języka C# zawierająca tablicę nie zawiera elementów tablicy. Zamiast tego struktura zawiera odwołanie do elementów. Tablicę o stałym rozmiarze można osadzić w strukturę, gdy jest używana w niebezpiecznym bloku kodu.
Rozmiar poniższych struct
elementów nie zależy od liczby elementów w tablicy, ponieważ pathName
jest to odwołanie:
public struct PathArray
{
public char[] pathName;
private int reserved;
}
Struktura może zawierać tablicę osadzoną w niebezpiecznym kodzie. W poniższym przykładzie tablica fixedBuffer
ma stały rozmiar. Aby uzyskać wskaźnik do pierwszego elementu, należy użyć fixed
instrukcji . Dostęp do elementów tablicy można uzyskać za pośrednictwem tego wskaźnika. Instrukcja fixed
przypina fixedBuffer
pole wystąpienia do określonej lokalizacji w pamięci.
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]);
}
}
Rozmiar tablicy elementów 128 char
wynosi 256 bajtów. Bufory znaków o stałym rozmiarze zawsze przyjmują 2 bajty na znak, niezależnie od kodowania. Ten rozmiar tablicy jest taki sam, nawet jeśli bufory znaków są ułożone do metod interfejsu API lub struktur za pomocą CharSet = CharSet.Auto
polecenia lub CharSet = CharSet.Ansi
. Aby uzyskać więcej informacji, zobacz CharSet.
W poprzednim przykładzie pokazano uzyskiwanie dostępu do fixed
pól bez przypinania. Inną typową tablicą o stałym rozmiarze jest tablica logiczna . Elementy tablicy bool
mają zawsze rozmiar 1 bajt. bool
tablice nie są odpowiednie do tworzenia tablic bitowych ani buforów.
Bufory o stałym rozmiarze System.Runtime.CompilerServices.UnsafeValueTypeAttributesą kompilowane za pomocą elementu , który instruuje środowisko uruchomieniowe języka wspólnego (CLR), że typ zawiera tablicę niezarządzaną, która może potencjalnie przepełnić. Pamięć przydzielona przy użyciu obiektu stackalloc automatycznie włącza również funkcje wykrywania przepełnień buforu w środowisku CLR. W poprzednim przykładzie pokazano, jak w obiekcie może istnieć bufor o stałym rozmiarze unsafe struct
.
internal unsafe struct Buffer
{
public fixed char fixedBuffer[128];
}
Język C# Buffer
wygenerowany przez kompilator jest przypisywany w następujący sposób:
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;
}
Bufory o stałym rozmiarze różnią się od zwykłych tablic w następujący sposób:
- Może być używany tylko w
unsafe
kontekście. - Mogą to być tylko pola wystąpień struktur.
- Są to zawsze wektory lub tablice jednowymiarowe.
- Deklaracja powinna zawierać długość, taką jak
fixed char id[8]
. Nie można użyć poleceniafixed char id[]
.
Użycie wskaźników do kopiowania tablicy bajtów
W poniższym przykładzie użyto wskaźników do skopiowania bajtów z jednej tablicy do innej.
W tym przykładzie użyto niebezpiecznego słowa kluczowego, które umożliwia używanie wskaźników w metodzie Copy
. Stała instrukcja służy do deklarowania wskaźników do tablic źródłowych i docelowych. Instrukcja fixed
przypina lokalizację tablic źródłowych i docelowych w pamięci, aby nie były przenoszone przez odzyskiwanie pamięci. Bloki pamięci dla tablic są odpinane po zakończeniu fixed
bloku. Ponieważ metoda w tym przykładzie Copy
używa słowa kluczowego unsafe
, należy ją skompilować przy użyciu opcji kompilatora AllowUnsafeBlocks .
W tym przykładzie uzyskuje dostęp do elementów obu tablic przy użyciu indeksów, a nie drugiego niezarządzanego wskaźnika. Deklaracja pSource
wskaźników i pTarget
przypina tablice.
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
*/
}
Wskaźniki funkcji
Język C# udostępnia delegate
typy do definiowania bezpiecznych obiektów wskaźnika funkcji. Wywoływanie delegata polega na utworzeniu wystąpienia typu pochodzącego z System.Delegate metody i wywołaniu metody wirtualnej do jej Invoke
metody. To wywołanie wirtualne używa instrukcji callvirt
IL. W ścieżkach kodu krytycznego calli
dla wydajności użycie instrukcji IL jest bardziej wydajne.
Wskaźnik funkcji można zdefiniować przy użyciu delegate*
składni. Kompilator wywoła funkcję przy użyciu calli
instrukcji, a nie utworzy wystąpienia obiektu i wywoła metodę Invoke
delegate
. Poniższy kod deklaruje dwie metody, które używają klasy delegate
lub delegate*
do łączenia dwóch obiektów tego samego typu. Pierwsza metoda używa typu delegata System.Func<T1,T2,TResult> . Druga metoda używa delegate*
deklaracji z tymi samymi parametrami i zwracanym typem:
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);
Poniższy kod pokazuje, jak zadeklarować statyczną funkcję lokalną i wywołać UnsafeCombine
metodę przy użyciu wskaźnika do tej funkcji lokalnej:
static int localMultiply(int x, int y) => x * y;
int product = UnsafeCombine(&localMultiply, 3, 4);
Powyższy kod ilustruje kilka reguł funkcji, do których uzyskuje się dostęp jako wskaźnik funkcji:
- Wskaźniki funkcji można zadeklarować tylko w
unsafe
kontekście. - Metody, które przyjmują metodę
delegate*
(lub zwracajądelegate*
wartość ), mogą być wywoływane tylko wunsafe
kontekście. - Operator
&
uzyskiwania adresu funkcji jest dozwolony tylko wstatic
przypadku funkcji. (Ta reguła ma zastosowanie zarówno do funkcji składowych, jak i funkcji lokalnych).
Składnia zawiera podobieństwa do deklarowania delegate
typów i używania wskaźników. Sufiks *
na delegate
wskazuje deklarację jest wskaźnikiem funkcji. Podczas &
przypisywania grupy metod do wskaźnika funkcji wskazuje, że operacja przyjmuje adres metody.
Możesz określić konwencję delegate*
wywoływania dla elementu przy użyciu słów kluczowych managed
i unmanaged
. Ponadto w przypadku unmanaged
wskaźników funkcji można określić konwencję wywoływania. W poniższych deklaracjach przedstawiono przykłady każdego z nich. Pierwsza deklaracja używa managed
konwencji wywoływania, która jest domyślna. Następne cztery używają unmanaged
konwencji wywoływania. Każdy określa jedną z konwencji wywołujących ECMA 335: Cdecl
, Stdcall
, Fastcall
lub Thiscall
. Ostatnia deklaracja używa unmanaged
konwencji wywoływania, poinstruując CLR, aby wybrać domyślną konwencję wywoływania dla platformy. ClR wybierze konwencję wywoływania w czasie wykonywania.
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);
Więcej informacji na temat wskaźników funkcji można znaleźć w specyfikacji funkcji wskaźnika funkcji.
specyfikacja języka C#
Aby uzyskać więcej informacji, zobacz rozdział Niebezpieczny kod specyfikacji języka C#.