Udostępnij za pośrednictwem


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, outlub 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, floatlub 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 structmoż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 fixedwyprowadza 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ście unsafe.
  • Operator & uzyskiwania adresu funkcji jest dozwolony tylko w funkcjach static. (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, Fastcalllub 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# .