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ů. Nepřiděluje také nezpracovanou paměť. Místo toho vytváří spravované objekty.

Jazyk C# podporuje unsafe kontext, ve kterém můžete napsat neověřitelný kód. unsafe V kontextu 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 nelze 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 odebráním kontrol maticových hranic.
  • 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í kompilátoru AllowUnsafeBlocks .

Typy ukazatelů

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

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

Typ zadaný před * typem ukazatele se nazývá odkazující typ. Pouze nespravovaný typ může být odkazující typ.

Typy ukazatelů nedědí z objektu a neexistují žádné převody mezi typy ukazatelů a object. Také boxování a rozbalování nepodporuje ukazatele. Můžete však převádět mezi různými typy ukazatele, nebo mezi různými typy ukazatele a integrálními typy.

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

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

Ukazatel nemůže odkazovat na odkaz nebo na strukturu , která obsahuje odkazy, protože odkaz na objekt může být uvolněn z paměti, i když ukazatel ukazuje na něj. Systém uvolňování paměti neuchovává přehled o tom, jestli je objekt odkazován libovolnými typy ukazatelů.

Hodnota proměnné ukazatele typu MyType* je adresa proměnné typu MyType. Následují příklady deklarace 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ředpokládejme například následující deklaraci:

int* myVariable;

Výraz *myVariable označuje proměnnou, která int se nachází na adrese obsažené v myVariable.

V článcích o fixed příkazu je několik příkladů ukazatelů. Následující příklad používá unsafe klíčové slovo a fixed příkaz a ukazuje, jak zvýšit vnitřní ukazatel. Tento kód spustíte vložením do funkce Main konzolové aplikace. Tyto příklady musí být zkompilovány pomocí sady možností kompilátoru 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
*/

Operátor nepřímého převodu nelze použít u ukazatele typu void*. Můžete však použít přetypování neplatného ukazatele a převést jej na jiný typ ukazatele, a naopak.

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

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

V následující tabulce je uveden seznam operátorů a příkazů, které mohou fungovat u ukazatelů v nezabezpečeném kontextu:

Operátor/Příkaz Používání
* Provádí dereferenci ukazatele.
-> Zpřístupňuje člen struktury prostřednictvím ukazatele.
[] Indexuje ukazatel.
& Získá adresu proměnné.
++ a -- Zvýší a sníží ukazatele.
+ a - Provádí aritmetické operace ukazatele.
==, !=, <, >, <=, a >= Porovnává ukazatele.
stackalloc Přidělí paměť v zásobníku.
fixed Prohlášení Dočasně pevně stanoví proměnnou tak, aby bylo možné vyhledat její adresu.

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

Jakýkoli typ ukazatele lze implicitně převést na void* typ. 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 přetypování. 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 na int* .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í

Pomocí klíčového fixed slova můžete vytvořit vyrovnávací paměť s polem s pevnou velikostí 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. Vyrovnávací paměť s pevnou velikostí 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 , , , , , , , sbyte, ushortuint, ulong, floatnebo .doublelongintshortcharbytebool

private fixed char name[30];

V bezpečném kódu neobsahuje struktura jazyka C#, která obsahuje pole, prvky pole. Struktura obsahuje odkaz na prvky. Pole s pevnou velikostí můžete vložit do struktury, když se používá v nebezpečném bloku kódu.

Velikost následujících struct prvků nezávisí na počtu prvků v poli, protože pathName se jedná o 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 fixedBuffer má pole pevnou velikost. Pomocí fixed příkazu získáte ukazatel na první prvek. K prvkům pole se dostanete pomocí tohoto ukazatele. Příkaz fixed připne fixedBuffer pole instance do konkrétního 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 prvků char 128 je 256 bajtů. Vyrovnávací paměti znaky s pevnou velikostí vždy zabírají 2 bajty na každý znak bez ohledu na kódování. Tato velikost pole je stejná i v případě, že 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 na webu CharSet.

Předchozí příklad ukazuje přístup k polím fixed bez připnutí. Dalším běžným polem s pevnou velikostí je logická matice. Prvky v bool poli jsou vždy o velikosti 1 bajtu. bool pole nejsou vhodná pro vytváření bitových polí nebo vyrovnávacích pamětí.

Vyrovnávací paměti s pevnou velikostí se kompilují pomocí System.Runtime.CompilerServices.UnsafeValueTypeAttributemodulu CLR (Common Language Runtime), který 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í stackallocu také automaticky umožňuje funkce detekce přetečení vyrovnávací paměti v CLR. Předchozí příklad ukazuje, jak může existovat vyrovnávací paměť s pevnou velikostí v objektu unsafe struct.

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

Kompilátor vygenerovaný jazykem C# pro Buffer je přiřazen následujícím způsobem:

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žívat ukazatele ke kopírování pole bajtů

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

Tento příklad používá nebezpečné klíčové slovo, které umožňuje použít ukazatele v Copy metodě. Pevný příkaz slouží k deklaraci ukazatelů na zdrojová a cílová pole. Příkaz fixed připne umístění zdrojových a cílových polí v paměti, aby se nepřesunuly uvolňováním paměti. Bloky paměti polí se po dokončení bloku odepnou fixed . Protože metoda v tomto příkladu Copyunsafe používá klíčové slovo, musí být zkompilována s AllowUnsafeBlocks možnost kompilátoru.

Tento příklad přistupuje k prvkům obou polí pomocí indexů místo druhého nespravovaného ukazatele. pSource Deklarace a pTarget ukazatele připne 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
    */
}

Ukazatelé funkcí

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

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

static int localMultiply(int x, int y) => x * y;
int 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 unsafe kontextu.
  • Metody, které přebírají delegate* (nebo vrací delegate*) lze volat pouze v unsafe kontextu.
  • Operátor & pro získání adresy funkce je povolen pouze u static funkcí. (Toto pravidlo platí pro členské funkce i místní funkce.

Syntaxe má paralelně s deklarací delegate typů a pomocí ukazatelů. Přípona * udávající delegate deklaraci je ukazatel funkce. Při & přiřazování skupiny metod ukazateli funkce označuje, že operace přebírá adresu metody.

Můžete zadat konvenci volání pro delegate* použití klíčových slov managed a unmanaged. Kromě toho můžete pro unmanaged ukazatele funkce 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í. Následující čtyři používají unmanaged konvenci volání. Každá 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 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);

Další informace o ukazatelích funkcí najdete ve specifikaci funkce ukazatele funkce.

specifikace jazyka C#

Další informace najdete v kapitole Nebezpečný kód specifikace jazyka C#.