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ż surowej pamięci. Zamiast tego tworzy zarządzane obiekty.
Język C# obsługuje kontekst unsafe
, w którym można napisać nieweryfikowalny kod. W kontekście unsafe
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 tylko kod, którego bezpieczeństwa nie można zweryfikować.
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 sprawdzenia granic tablic.
- 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źników
W niebezpiecznym kontekście typ może być typem wskaźnika, jak również typem wartości lub typem odwołania. Deklaracja typu wskaźnika przyjmuje jedną z następujących form:
type* identifier;
void* identifier; //allowed but not recommended
Typ określony przed *
w typie wskaźnika jest nazywany typem odniesienia.
Typy wskaźników nie dziedziczą z obiektu i nie istnieją konwersje między typami wskaźników a object
. Ponadto boxing i unboxing nie obsługują wskaźników. Można jednak konwertować między różnymi typami wskaźników i między typami wskaźników i 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#
Moduł odśmiecający śmieci nie śledzi, czy obiekt jest wskazywany przez jakiekolwiek typy wskaźników. Jeśli odwołanie jest obiektem w zarządzanym stercie (w tym zmiennych lokalnych przechwyconych przez wyrażenia lambda lub anonimowych delegatów), obiekt musi być przypięty tak długo, jak wskaźnik jest używany.
Wartość zmiennej wskaźnika typu MyType*
jest adresem zmiennej typu MyType
. Poniżej przedstawiono przykłady deklaracji typów wskaźników:
-
int* p
:p
jest wskaźnikiem do liczby całkowitej. -
int** p
:p
jest wskaźnikiem do wskaźnika liczby całkowitej. -
int*[] p
:p
jest jednowymiarową tablicą wskaźników do liczb całkowitych. -
char* p
:p
jest wskaźnikiem do char. -
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. Rozważmy na przykład następującą deklarację:
int* myVariable;
Wyrażenie *myVariable
oznacza zmienną int
znalezioną pod adresem zawartym w myVariable
.
Istnieje kilka przykładów wskaźników w artykułach dotyczących stwierdzenia fixed
. W poniższym przykładzie użyto słowa kluczowego unsafe
i wyrażenia fixed
oraz pokazano, jak zwiększyć wskaźnik wewnętrzny. Możesz wkleić ten kod do funkcji Main aplikacji konsolowej, 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 dowolny inny typ wskaźnika i na odwrót.
Wskaźnik może być null
. Zastosowanie operatora pośredniego do wskaźnika o wartości null powoduje zachowanie zdefiniowane przez implementację.
Przekazywanie wskaźników między metodami może powodować niezdefiniowane zachowanie. Rozważ metodę zwracającą wskaźnik do zmiennej lokalnej przez parametr in
, out
lub ref
albo jako wynik funkcji. Jeśli wskaźnik został ustawiony w stałym bloku, zmienna, do której wskazuje, nie może być już stała.
W poniższej tabeli wymieniono operatory i instrukcje, które mogą działać na wskaźnikach w niebezpiecznym kontekście:
Operator/instrukcja | Użyj |
---|---|
* |
Wykonuje dereferencję wskaźnika. |
-> |
Uzyskuje dostęp do elementu członkowskiego struktury za pomocą wskaźnika. |
[] |
Indeksuje wskaźnik. |
& |
Uzyskuje adres zmiennej. |
++ i -- |
Wskaźniki zostają inkrementowane i dekrementowane. |
+ i - |
Wykonuje arytmetykę wskaźników. |
== , != , < , > , <= i >= |
Porównuje wskaźniki. |
stackalloc |
Przydziela pamięć na stosie. |
fixed oświadczenie |
Tymczasowo naprawia zmienną, aby można było znaleźć jej adres. |
Aby uzyskać więcej informacji na temat operatorów wskaźnikowych, zobacz sekcję Operatory wskaźnikowe.
Dowolny typ wskaźnika można niejawnie przekonwertować na typ void*
. Do każdego 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. Te konwersje wymagają jawnego rzutowania.
Poniższy przykład konwertuje int*
na byte*
. Zwróć uwagę, że wskaźnik wskazuje najniższy adresowany bajt zmiennej. Po kolejnych przyrostach wyniku do rozmiaru int
(4 bajty) 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
Za pomocą słowa kluczowego fixed
możesz utworzyć bufor 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
, byte
, char
, short
, int
, long
, sbyte
, ushort
, uint
, ulong
, 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. Możesz osadzić tablicę o stałym rozmiarze w strukturze , gdy jest używana w niebezpiecznym bloku kodu .
Rozmiar następujących struct
nie zależy od liczby elementów w tablicy, ponieważ pathName
jest odwołaniem:
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ć instrukcji fixed
. Dostęp do elementów tablicy można uzyskać za pośrednictwem tego wskaźnika. Instrukcja fixed
przypina pole wystąpienia fixedBuffer
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 128 elementów char
wynosi 256 bajtów. Bufory znaków o stałym rozmiarze zawsze zajmują 2 bajty na znak, niezależnie od kodowania. Ten rozmiar tablicy pozostaje taki sam, nawet gdy bufory znaków są przekazywane do metod interfejsu API lub struktur z CharSet = CharSet.Auto
lub CharSet = CharSet.Ansi
. Aby uzyskać więcej informacji, zobacz CharSet.
W poprzednim przykładzie pokazano uzyskiwanie dostępu do pól fixed
bez przypinania. Inną typową tablicą o stałym rozmiarze jest tablica bool. Elementy w tablicy bool
mają zawsze rozmiar 1 bajt.
bool
tablice nie są odpowiednie do tworzenia tablic bitowych ani buforów.
Bufory o stałym rozmiarze są kompilowane za pomocą System.Runtime.CompilerServices.UnsafeValueTypeAttribute, który informuje środowisko uruchomieniowe języka wspólnego (CLR), że typ zawiera niezarządzaną tablicę, która może ulec przepełnieniu. Pamięć przydzielona przy użyciu stackalloc również automatycznie włącza funkcje wykrywania przepełnień buforu w środowisku CLR. W poprzednim przykładzie pokazano, jak w unsafe struct
może istnieć bufor o stałym rozmiarze.
internal unsafe struct Buffer
{
public fixed char fixedBuffer[128];
}
Wygenerowany przez kompilator język C# dla Buffer
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 kontekście
unsafe
. - 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ćfixed char id[]
.
Jak używać 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 słowa kluczowego niebezpiecznego, które umożliwia używanie wskaźników w metodzie Copy
. Instrukcja fixed służy do deklarowania wskaźników do tablic źródłowych i docelowych. Instrukcja fixed
wyprowadza lokalizację tablic źródłowych i docelowych w pamięci, aby odzyskiwanie pamięci nie przenosiło tablic. Bloki pamięci dla tablic są odpinane, gdy blok fixed
zostanie ukończony. Ponieważ metoda Copy
w tym przykładzie używa słowa kluczowego unsafe
, należy go skompilować przy użyciu allowUnsafeBlocks opcji kompilatora.
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 wskaźników pSource
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 typy delegate
do definiowania bezpiecznych obiektów wskaźnika funkcji. Wywoływanie delegata polega na utworzeniu instancji typu pochodzącego z System.Delegate i wykonaniu wywołania metody wirtualnej do jej metody Invoke
. To wywołanie wirtualne używa instrukcji callvirt
IL. W ścieżkach kodu krytycznego dla wydajności użycie instrukcji calli
IL jest bardziej wydajne.
Wskaźnik funkcji można zdefiniować przy użyciu składni delegate*
. Kompilator wywołuje funkcję przy użyciu instrukcji calli
zamiast instancjonowania obiektu delegate
i wywoływania Invoke
. Poniższy kod deklaruje dwie metody używające 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 deklaracji delegate*
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 unsafe 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ć metodę UnsafeCombine
przy użyciu wskaźnika do tej funkcji lokalnej:
int product = 0;
unsafe
{
static int localMultiply(int x, int y) => x * y;
product = UnsafeCombine(&localMultiply, 3, 4);
}
Poprzedni kod ilustruje kilka reguł dotyczących dostępu do funkcji za pomocą wskaźnika funkcji.
- Wskaźniki funkcji można zadeklarować tylko w kontekście
unsafe
. - Metody, które przyjmują
delegate*
(lub zwracajądelegate*
), mogą być wywoływane tylko w kontekścieunsafe
. - Operator
&
uzyskiwania adresu funkcji jest dozwolony tylko w funkcjachstatic
. (Ta reguła ma zastosowanie zarówno do funkcji składowych, jak i funkcji lokalnych).
Składnia zawiera podobieństwa do deklarowania typów delegate
i używania wskaźników. Sufiks *
delegate
wskazuje, że deklaracja jest wskaźnikiem funkcji .
&
podczas przypisywania grupy metod do wskaźnika funkcji wskazuje, że operacja przyjmuje adres metody.
Konwencję wywoływania dla delegate*
można określić przy użyciu słów kluczowych managed
i unmanaged
. Ponadto w przypadku wskaźników funkcji unmanaged
można określić konwencję wywoływania. W poniższych deklaracjach przedstawiono przykłady każdego z nich. Pierwsza deklaracja używa konwencji wywoływania managed
, która jest domyślna. Następne cztery używają konwencji wywołań unmanaged
. Każdy określa jedną z konwencji wywołujących ECMA 335: Cdecl
, Stdcall
, Fastcall
lub Thiscall
. Ostatnia deklaracja używa konwencji wywoływania unmanaged
, poinstruując CLR, aby wybrać domyślną konwencję wywoływania dla platformy. CLR wybiera konwencję wywoływania podczas wykonywania.
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);
Więcej informacji na temat wskaźników do funkcji można znaleźć w specyfikacji funkcji wskazującej .
Specyfikacja języka C#
Aby uzyskać więcej informacji, zobacz Niebezpieczny kod rozdział specyfikacji języka C# .