Sdílet prostřednictvím


Nebezpečný kód, typy ukazatelů a ukazatele funkcí

Většina kódu jazyka C#, který napíšete, je "ověřitelně bezpečný kód". Ověřitelně bezpečný kód znamená, že nástroje .NET můžou ověřit, že je kód bezpečný. Obecně platí, že bezpečný kód nemá přímý přístup k paměti pomocí ukazatelů. Také nepřiděluje surovou paměť. Místo toho vytváří spravované objekty.

Jazyk C# podporuje kontext unsafe, ve kterém můžete psát neověřitelný kód. V kontextu unsafe může kód používat ukazatele, přidělovat a uvolnit bloky paměti a volat metody pomocí ukazatelů funkce. Nebezpečný kód v jazyce C# nemusí být nutně nebezpečný; je to jenom kód, jehož bezpečnost se nedá ověřit.

Nebezpečný kód má následující vlastnosti:

  • Metody, typy a bloky kódu je možné definovat jako nebezpečné.
  • V některých případech může nebezpečný kód zvýšit výkon aplikace tím, že odstraní kontrolu hranic pole.
  • Nebezpečný kód je vyžadován při volání nativních funkcí, které vyžadují ukazatele.
  • Použití nebezpečného kódu představuje rizika zabezpečení a stability.
  • Kód, který obsahuje nebezpečné bloky, musí být zkompilován s možností AllowUnsafeBlocks kompilátoru.

Typy ukazatelů

V nezabezpečeném kontextu může být typ, vedle typu hodnoty nebo odkazu, typem ukazatele. Deklarace typu ukazatele má jednu z následujících forem:

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

Typ zadaný před * v typu ukazatele se nazývá odkazující typ.

Typy ukazatelů nedědí z objektu a mezi typy ukazatelů a objectneexistují žádné převody . Také boxování a rozbalování nepodporují ukazatele. Můžete ale převést mezi různými typy ukazatelů a mezi typy ukazatelů a integrálními typy.

Když deklarujete více ukazatelů ve stejné deklaraci, napíšete hvězdičku (*) společně s základním typem. Nepoužívá se jako předpona názvu každého ukazatele. Například:

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

Uvolňovač paměti nesleduje, zda je objekt odkazován jakýmikoli typy ukazatelů. Pokud je referent objektem ve spravované haldě (včetně místních proměnných zachycených výrazy lambda nebo anonymními delegáty), musí být objekt připnut, pokud se používá ukazatel.

Hodnota proměnné ukazatele typu MyType* je adresa proměnné typu MyType. Tady jsou příklady deklarací typu ukazatele:

  • int* p: p je ukazatel na celé číslo.
  • int** p: p je ukazatel na ukazatel na celé číslo.
  • int*[] p: p je jednorozměrné pole ukazatelů na celá čísla.
  • char* p: p je ukazatel na znak.
  • void* p: p je ukazatel na neznámý typ.

Operátor nepřímých ukazatelů * lze použít pro přístup k obsahu v umístění, na které odkazuje proměnná ukazatele. Představte si například následující deklaraci:

int* myVariable;

Výraz *myVariable označuje proměnnou int nalezenou na adrese obsažené v myVariable.

V článcích o příkazu fixedexistuje několik příkladů ukazatelů . Následující příklad používá klíčové slovo unsafe a příkaz fixed a ukazuje, jak zvýšit vnitřní ukazatel. Tento kód můžete vložit do hlavní funkce konzolové aplikace a spustit ho. Tyto příklady musí být zkompilovány pomocí AllowUnsafeBlocks sada možností kompilátoru.

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

U ukazatele typu void*nelze použít operátor nepřímého přístupu. Přesto můžete použít přetypování k převedení ukazatele typu void na jakýkoli jiný typ ukazatele a naopak.

Ukazatel může být null. Použití operátoru indirection na ukazatel null způsobí chování definované implementací.

Předávání ukazatelů mezi metodami může způsobit nedefinované chování. Představte si metodu, která vrací ukazatel na místní proměnnou prostřednictvím in, outnebo ref parametru nebo jako výsledek funkce. Pokud byl ukazatel nastaven v pevném bloku, proměnná, na kterou odkazuje, již nemusí být opravena.

Následující tabulka uvádí operátory a příkazy, které mohou pracovat s ukazateli v nebezpečném kontextu:

Operátor/Výrok Použít
* Provádí dereferenci ukazatele.
-> Přistupuje k členu struktury prostřednictvím ukazatele.
[] Indexuje ukazatel.
& Získá adresu proměnné.
++ a -- Přírůstky a dekrementace ukazatelů.
+ a - Provádí aritmetiku s ukazateli.
==, !=, <, >, <=a >= Porovnává ukazatele.
stackalloc Přidělí paměť v zásobníku.
fixed příkazu Dočasně opraví proměnnou, aby byla nalezena její adresa.

Další informace o operátorech souvisejících s ukazateli najdete v tématu operátory související s ukazatelem.

Jakýkoli typ ukazatele lze implicitně převést na typ void*. Každému typu ukazatele lze přiřadit hodnotu null. Libovolný typ ukazatele lze explicitně převést na jakýkoli jiný typ ukazatele pomocí výrazu typu casting. Můžete také převést jakýkoli celočíselný typ na typ ukazatele nebo jakýkoli typ ukazatele na celočíselný typ. Tyto převody vyžadují explicitní přetypování.

Následující příklad převede int* na byte*. Všimněte si, že ukazatel odkazuje na nejnižší adresovaný bajt proměnné. Když postupně zvýšíte výsledek, až do velikosti int (4 bajty), můžete zobrazit zbývající bajty proměnné.

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

Vyrovnávací paměti s pevnou velikostí

Klíčové slovo fixed můžete použít k vytvoření vyrovnávací paměti s polem pevné velikosti v datové struktuře. Vyrovnávací paměti s pevnou velikostí jsou užitečné při psaní metod, které interoperují se zdroji dat z jiných jazyků nebo platforem. Pevně veliký buffer může přijímat jakékoli atributy nebo modifikátory, které jsou povoleny pro běžné členy struktury. Jediným omezením je, že typ pole musí být bool, byte, char, short, int, long, sbyte, ushort, uint, ulong, floatnebo double.

private fixed char name[30];

Ve strukturovaném jazyce C# v bezpečném kódu, pokud struktura obsahuje pole, toto pole neobsahuje prvky. Struktura místo toho obsahuje odkaz na prvky. Pole s pevnou velikostí můžete vložit do struktury , pokud se používá v nebezpečném bloku kódu .

Velikost následujících struct nezávisí na počtu prvků v poli, protože pathName je odkaz:

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

Struktura může obsahovat vložené pole v nebezpečném kódu. V následujícím příkladu má pole fixedBuffer pevnou velikost. Pomocí příkazu fixed získáte ukazatel na první prvek. K prvkům pole se dostanete pomocí tohoto ukazatele. Příkaz fixed připne pole instance fixedBuffer k určitému umístění v paměti.

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

Velikost pole char se 128 prvky je 256 bajtů. Pevná velikost znakové bufferů vždy vyžaduje 2 bajty na každý znak, bez ohledu na kódování. Tato velikost pole je stejná, i když jsou vyrovnávací paměti znaků zařazovány do metod rozhraní API nebo struktur s CharSet = CharSet.Auto nebo CharSet = CharSet.Ansi. Další informace najdete v tématu CharSet.

Předchozí příklad ukazuje přístup k polím fixed bez přiřazení. Dalším běžným polem s pevnou velikostí je boolovské pole. Prvky v poli bool mají vždy velikost 1 bajt. bool pole nejsou vhodná pro tvorbu bitových polí nebo vyrovnávacích pamětí.

Vyrovnávací paměti s pevnou velikostí se kompilují pomocí System.Runtime.CompilerServices.UnsafeValueTypeAttribute, což dává modulu CLR (Common Language Runtime) pokyn, že typ obsahuje nespravované pole, které může potenciálně přetékat. Paměť přidělená pomocí stackalloc také automaticky povolí funkce detekce přetečení vyrovnávací paměti v CLR. Předchozí příklad ukazuje, jak může v unsafe structexistovat vyrovnávací paměť s pevnou velikostí.

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

Kompilátor vygenerovaný jazykem C# pro Buffer má následující atributy:

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

Vyrovnávací paměti s pevnou velikostí se liší od běžných polí následujícími způsoby:

  • Lze použít pouze v unsafe kontextu.
  • Mohou to být pouze pole instancí struktur.
  • Jsou to vždy vektory nebo jednorozměrná pole.
  • Deklarace by měla obsahovat délku, například fixed char id[8]. Nemůžete použít fixed char id[].

Jak použít ukazatele ke zkopírování pole bajtů

Následující příklad používá ukazatele ke kopírování bajtů z jednoho pole do druhého.

V tomto příkladu se používá klíčové slovo nebezpečné, které umožňuje používat ukazatele v metodě Copy. Příkaz typu se používá k deklaraci ukazatelů na zdrojová a cílová pole. Příkaz fixedpřipne umístění zdrojových a cílových polí v paměti, aby uvolňování paměti nepřesunuly pole. Bloky paměti polí jsou při dokončení bloku fixed odepnuty. Protože metoda Copy v tomto příkladu používá klíčové slovo unsafe, musí být zkompilována s přepínačem kompilátoru AllowUnsafeBlocks.

Tento příklad přistupuje k prvkům obou polí pomocí indexů místo druhého nespravovaného ukazatele. Deklarace ukazatelů pSource a pTarget uzamkne pole.

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

Ukazatele na funkce

Jazyk C# poskytuje delegate typy pro definování bezpečných funkčních ukazatelů. Vyvolání delegáta zahrnuje vytvoření instance typu odvozeného z System.Delegate a volání virtuální metody do metody Invoke. Toto virtuální volání používá instrukce callvirt IL. V cestách kódu kritických pro výkon je použití instrukce calli IL efektivnější.

Ukazatel funkce můžete definovat pomocí syntaxe delegate*. Kompilátor volá funkci pomocí instrukce calli místo vytvoření instance objektu delegate a volání Invoke. Následující kód deklaruje dvě metody, které používají delegate nebo delegate* ke kombinování dvou objektů stejného typu. První metoda používá typ delegáta System.Func<T1,T2,TResult>. Druhá metoda používá deklaraci delegate* se stejnými parametry a návratovým 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);

Následující kód ukazuje, jak deklarovat statickou místní funkci a vyvolat metodu UnsafeCombine pomocí ukazatele na tuto místní funkci:

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

Předchozí kód znázorňuje několik pravidel funkce, ke kterým se přistupuje jako ukazatel funkce:

  • Ukazatele funkce lze deklarovat pouze v kontextu unsafe.
  • Metody, které přebírají delegate* (nebo vrací delegate*), lze volat pouze v kontextu unsafe.
  • Operátor & k získání adresy funkce je povolen pouze u funkcí static. (Toto pravidlo platí pro členské funkce i místní funkce.

Syntaxe má paralely s deklarací typů delegate a používáním ukazatelů. Přípona *delegate označuje, že deklarace je ukazatel funkce. & při přiřazování skupiny metod ukazateli funkce označuje, že operace přebírá adresu metody.

Konvenci volání pro delegate* můžete zadat pomocí klíčových slov managed a unmanaged. Kromě toho můžete pro ukazatele funkce unmanaged určit konvenci volání. Následující deklarace ukazují příklady každého z nich. První deklarace používá managed konvenci volání, což je výchozí. Další čtyři používají konvenci volání unmanaged. Každá z nich určuje jednu z konvencí volání ECMA 335: Cdecl, Stdcall, Fastcallnebo Thiscall. Poslední deklarace používá unmanaged konvenci volání a dává CLR pokyn k výběru výchozí konvence volání pro platformu. CLR zvolí konvenci volání za běhu.

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

Další informace o ukazatelích na funkce najdete ve specifikaci funkce ukazatel na funkci.

Specifikace jazyka C#

Další informace naleznete v kapitole nebezpečného kódu specifikace jazyka C#.