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

Většina kódu jazyka C#, který píšete, je ověřitelně bezpečný kód. Ověřitelně bezpečný kód znamená, že .NET nástroje 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.

Referenční dokumentace jazyka C# dokumentuje naposledy vydané verze jazyka C#. Obsahuje také počáteční dokumentaci k funkcím ve verzi Public Preview pro nadcházející jazykovou verzi.

Dokumentace identifikuje všechny funkce, které byly poprvé představeny v posledních třech verzích jazyka nebo v aktuálních verzích Public Preview.

Návod

Informace o tom, kdy byla funkce poprvé představena v jazyce C#, najdete v článku o historii verzí jazyka C#.

Jazyk C# také podporuje unsafe kontext, ve kterém můžete napsat neověřitelný kód. Nebezpečný kód nemusí být nutně nebezpečný; je to jenom kód, jehož bezpečnost nemůže být ověřena nástroji .NET. Nebezpečný kód slouží k volání nativních funkcí, které vyžadují ukazatele, a v některých případech ke zlepšení výkonu prostřednictvím přímého přístupu k paměti, který zabraňuje kontrolám vázaných na pole. Nebezpečný kód také přináší rizika zabezpečení a stability. Chcete-li zkompilovat kód obsahující unsafe kontext, přidejte možnost kompilátoru AllowUnsafeBlocks .

Jazyk C# definuje dva modely pro to, co se počítá jako nebezpečný kód: původní model a aktualizovaný model bezpečnosti paměti ve verzi Preview v jazyce C# 15 a .NET 11. Informace o tom, jak se oba modely liší, najdete v tématu Dva modely pro nebezpečný kód.

Informace o osvědčených postupech pro nebezpečný kód v jazyce C# najdete v tématu Osvědčené postupy pro nebezpečný kód.

Dva modely pro nebezpečný kód

Jazyk C# definuje dva modely pro nebezpečný kód. Model ve výsledku určuje, které operace vyžadují unsafe kontext a jak unsafe modifikátor člena ovlivňuje volající.

  • Původní nebezpečný model: Kontext unsafe pokrývá existenci funkcí ukazatele. Deklarujete typ ukazatele, vezmete adresu proměnné, převeďte ukazatel, převedete stackalloc výraz na ukazatel nebo použijete sizeof pouze u libovolného typu v unsafe kontextu. (Výraz stackalloc přiřazený nebo ReadOnlySpan<T> je povolený v bezpečném Span<T> kódu.) unsafe Modifikátor typu, člena nebo bloku stanoví tento kontext, ale nezavazuje volající. Jazyk C# 1.0 zavedl tento model a zůstává výchozím.
  • Aktualizovaný model zabezpečení paměti: Kontext unsafe zahrnuje operace, které přistupují k paměti, které modul runtime nespravuje. Existence ukazatele není nebezpečná; dereference ukazatele je. unsafe Modifikátor člena se stane smlouvou, která rozšíří povinnost auditovat bezpečnost volajícího. Tento model je ve verzi Preview v jazyce C# 15 a .NET 11.

Následující tabulka porovnává, které operace vyžadují unsafe kontext v každém modelu.

Operation Původní model Aktualizovaný model
Deklarace typu ukazatele nebo převzetí adresy pomocí & Vyžaduje unsafe Povoleno v bezpečném kódu
Příkaz fixed Vyžaduje unsafe Povoleno v bezpečném kódu
Převedení výrazu stackalloc na ukazatel Vyžaduje unsafe Povoleno v bezpečném kódu
Operátor sizeof u libovolného nespravovaného typu Vyžaduje unsafe Povoleno v bezpečném kódu
Zprostředkování ukazatele (*p), přístup ke členům (p->m) nebo přístup k elementům (p[i]) Vyžaduje unsafe Vyžaduje unsafe
Vyvolání ukazatele funkce Vyžaduje unsafe Vyžaduje unsafe
Přístup k elementům v vyrovnávací paměti s pevnou velikostí Vyžaduje unsafe Vyžaduje unsafe
Volání člena označeného unsafe Žádný požadavek volajícího Vyžaduje unsafe

Pokud chcete vyzkoušet aktualizovaný model, použijte sadu .NET 11 SDK (ve verzi Preview) a nastavte možnost kompilátoru LangVersion na previewhodnotu . Odpočinek ukazatele platí při každém kompilaci pomocí kompilátoru C# 15 a preview jazykové verze. Úplné vynucování, včetně povinností volajícího a výslovného souhlasu se sestavením, je stále ve vývoji. Další informace najdete v aktualizovaném modelu zabezpečení paměti (Preview).

Původní nebezpečný model

V původním modelu unsafe klíčové slovo vytvoří nebezpečný kontext u typu, člena nebo bloku a tento kontext odemkne funkce ukazatele popsané v následujících částech. unsafe Modifikátor změní jenom to, co označený kód může dělat; neuděluje volajícím žádný požadavek. Pokud chcete zkompilovat některý z těchto příkladů, nastavte možnost kompilátoru AllowUnsafeBlocks .

Typy ukazatelů

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

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

Typ, který zadáte před * typem ukazatele, je odkazující typ.

Typy ukazatelů nedědí z objektu a mezi typy objectukazatelů a . 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, napište hvězdičku (*) společně s pouze 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 odkaz objektem ve spravované haldě (včetně místních proměnných zachycených výrazy lambda nebo anonymními delegáty), musíte objekt připnout tak dlouho, dokud se použije 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.

Pomocí operátoru nepřímých ukazatelů * můžete získat 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 můžete 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 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: {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í

Matice jsou odkazové typy, takže v bezpečném kódu pole struktury, které je polem, ukládá pouze odkaz na prvky pole, nikoli samotné prvky. Velikost následujících struct prvků nezávisí na počtu prvků v poli, protože pathName jde o odkaz:

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

Pokud chcete obsah pole uložit do samotné struktury, použijte fixed klíčové slovo k deklaraci vyrovnávací paměti s pevnou velikostí. Klíčové fixed slovo vyžaduje unsafe kontext. 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 , , , , , , , , ushortuint, ulong, floatnebo :doublesbytelongintshortcharbytebool

private fixed char name[30];

V následujícím příkladu má pole fixedBuffer pevnou velikost. Příkaz sloužífixed k získání ukazatele na první prvek a přístup k prvkům pole prostřednictvím 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 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í 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 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:

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

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í delegate* syntaxe. Kompilátor volá 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 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 deklarujete statickou místní funkci a vyvoláte 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 můžete deklarovat pouze v unsafe kontextu.
  • Můžete volat pouze metody, které přebírají delegate* (nebo vrací delegate*) v unsafe kontextu.
  • 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í můžete delegate* 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 funkcí najdete v části Ukazatele na funkce specifikace jazyka C#.

Příklad: Kopírování pole bajtů pomocí ukazatelů

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

V tomto příkladu unsafe se používá klíčové slovo, které umožňuje používat ukazatele v Copy metodě. Příkaz fixed deklaruje ukazatele 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. Blok fixed připne paměťové bloky pro pole v oboru bloku. Protože metoda v tomto příkladu Copyunsafe používá klíčové slovo, je nutné jej zkompilovat pomocí AllowUnsafeBlocks kompilátoru.

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

Aktualizovaný model bezpečnosti paměti (Preview)

Important

Aktualizovaný model bezpečnosti paměti je funkce preview v jazyce C# 15 a .NET 11. Pokračuje v vývoji na základě zpětné vazby během vydaných verzí Preview. K vyzkoušení modelu použijte sadu SDK .NET 11 (Preview) a nastavte možnost kompilátoru LangVersion na previewhodnotu . Kompilátor v .NET 11 Preview 5 implementuje uvolnění ukazatelů, ale zatím nevynucuje povinnosti volajícího, souhlas sestavení nebo safe klíčové slovo. Úplný návrh najdete ve specifikaci funkce bezpečnosti paměti.

Aktualizovaný model odděluje dvě věci, které původní model považuje za jednu: existenci kódu ukazatele a šíření bezpečnostních povinností volajícím. Označení člena unsafe už nepovoluje jen ukazatele v jeho těle; zneužívá volajícího, takže každý volající musí tuto povinnost buď rozšířit, nebo ho vypustit za ověřenou, bezpečnou volatelnou hranici. Aby bylo možné toto oddělení podpořit, model také zužuje nebezpečný kontext: existence ukazatele není nebezpečná, pouze operace, které přistupují k paměti modulu runtime, nespravuje. Zúžení umožňuje uchovávat, předávat a vracet ukazatele v bezpečném kódu, zatímco unsafe označí operace a členy, které mohou skutečně narušit bezpečnost paměti.

Nebezpečné členy volajícího

V původním modelu unsafe modifikátor na členu umožňuje pouze ukazatele v podpisu a textu člena. Neinformuje volající o bezpečnosti. Aktualizovaný model dává modifikačnímu významu pro volající. Když označíte člena unsafe, kompilátor ho považuje za nebezpečné volajícího (označované také jako povinné): každý volající ho musí vyvolat z unsafe kontextu a povinnost auditovat bezpečnost přesune do daného volajícího.

unsafe Modifikátor podpisu člena už pro tělo nezřídí nebezpečný kontext. Dvě role jsou rozdělené:

  • unsafe Modifikátor podpisu šíří povinnost volajícím.
  • Vnitřní unsafe blok definuje operace, které přistupují k nespravované paměti.

V následujícím náhledovém mockupu ReadInt32 je volající nebezpečný. Podpis má unsafe modifikátor a vnitřní unsafe blok zabalí dereference:

// Preview: illustrates the updated model, which the current compiler doesn't fully enforce yet.
public static unsafe int ReadInt32(byte* source)
{
    unsafe
    {
        return *(int*)source;
    }
}

Volající zabalí hovor do vlastního unsafe bloku:

// Preview
unsafe
{
    int value = ReadInt32(buffer);
}

Aktualizovaný model také zpřísňuje několik souvisejících pravidel:

  • unsafe Modifikátor vytvoří chybu u deklarace typu, statického konstruktoru a finalizátoru, protože modifikátor nemá k informování volajícího.
  • Delegáti nemohou být unsafe, protože delegát má tvar typu.
  • Typ, jehož konstruktor unsafe bez parametrů nevyhovuje new() omezení.

Operace, které vyžadují nebezpečný kontext

Operace, které přistupují k paměti odkazované na paměť, vyžadují unsafe kontext:

  • Nepřímý ukazatel (*p), přístup ke členům ukazatele (p->member) a přístup k prvku ukazatele (p[i]).
  • Vyvolání ukazatele funkce
  • Přístup k elementům v vyrovnávací paměti s pevnou velikostí

Následující příklad připne pole bez unsafe kontextu, ale přeskočí ukazatel uvnitř jednoho:

public static int ReadValue(int[] numbers)
{
    fixed (int* first = numbers)
    {
        // Dereferencing a pointer accesses unmanaged memory, so it still
        // requires an unsafe context.
        unsafe
        {
            return *first;
        }
    }
}

Uvolněné operace

Operace, které nepřistupují k paměti odkazované na paměť, už nevyžadují unsafe kontext:

  • Deklarování typu ukazatele a převzetí adresy proměnné pomocí operátoru &
  • Příkaz fixed , který připne proměnnou.
  • Převod výrazu stackalloc na ukazatel
  • Operátor sizeof použitý u jakéhokoli nespravovaného typu.

Následující příklad vytvoří a připne ukazatele bez unsafe kontextu:

public static void CreatePointer()
{
    int value = 42;
    // Creating a pointer doesn't require an unsafe context.
    int* pointer = &value;
    int** pointerToPointer = &pointer;
}
public static void PinArray(int[] numbers)
{
    // The fixed statement no longer requires an unsafe context.
    fixed (int* first = numbers)
    {
        int* current = first;
    }
}

Tyto odpočinek platí vždy, když kompilujete s jazykovou preview verzí, ať už se sestavení přihlásí k aktualizovaným pravidlům bezpečnosti paměti nebo ne.

Povinnosti vyřazené volajícím, které nejsou nebezpečné

Člen, který volá operaci nezabezpečenou volajícím, má dvě možnosti: rozšířit povinnost nebo ji vypustit.

  • Šíření: Označte svého vlastního člena unsafe. Povinnost projde vašim volajícím. Šíření použijte, pokud nemůžete plně ověřit povinnost sami.
  • Uvolnění: Nechte podpis svého člena v bezpečí. Ověřte povinnost uvnitř člena, obvykle s hlídanými moduly runtime, a pak proveďte nebezpečnou operaci ve vnitřním unsafe bloku. Člen, který obsahuje vnitřní unsafe blok, ale neoznačuje svůj vlastní podpis unsafe , je nebezpečná hranice: změní nebezpečný kód na bezpečný povrch.

Následující mockup ve verzi Preview ověří jeho vstup pomocí strážce, připne spravované pole a přečte ukazatel. Volající nepotřebují unsafe kontext, protože metoda vyhodí povinnost:

// Preview
public static int SumBytes(byte[] source)
{
    ArgumentNullException.ThrowIfNull(source);

    fixed (byte* first = source)
    {
        unsafe
        {
            // SAFETY: the null check and source.Length bound every read to the pinned array.
            int total = 0;
            for (int i = 0; i < source.Length; i++)
            {
                total += first[i];
            }

            return total;
        }
    }
}

Kontrola hodnoty null a délka pole vyloučí vstupy, které by umožnily spuštění čtení po vyrovnávací paměti, takže dereference uvnitř unsafe bloku je zvuk. Metoda neopouští žádné zbytkové závazky, takže zveřejňuje bezpečný podpis, který lze volat.

Dokumentace k bezpečnosti

Volající nebezpečný člen by měl zdokumentovat, co volající musí zaručit. Aktualizovaný model podporuje dva doplňkové styly komentářů:

  • Blok /// <safety> dokumentace nad podpisem uvádí formální smlouvu: podmínky, které volající musí splňovat. Analyzátor může označit volajícího nebezpečného člena, kterému chybí.
  • // SAFETY: Komentář uvnitř unsafe bloku zaznamenává, proč je operace na daném místě zvuková, pro vývojáře a auditory, kteří si tělo přečetli.

Následující náhledový mockup ukazuje oba styly metody nezabezpečené ReadByte volajícím:

// Preview
/// <summary>Reads a single byte from unmanaged memory.</summary>
/// <safety>
/// The sum of <paramref name="ptr"/> and <paramref name="offset"/> must address a byte
/// the caller is permitted to read.
/// </safety>
public static unsafe byte ReadByte(IntPtr ptr, int offset)
{
    byte* address = (byte*)ptr;
    unsafe
    {
        // SAFETY: relies on the caller obligation stated in the <safety> block.
        return address[offset];
    }
}

Blok /// <safety> vám řekne smlouvu. Smlouva patří do dokumentace, kde ji vidí každý volající a kontrolor.

Nebezpečná pole

unsafe Modifikátor pole použijte, pokud jeho deklarovaný typ nevyjádřuje kontrakty, které ohraničující typ udržuje a na jiném kódu závisí. Nebezpečné je v mezerě mezi tím, co systém typů vidí a jaký typ slibuje. Modifikátor vynutí každý zápis do pole do unsafe bloku, který udržuje zápisy na jednom místě.

Nejjasnější je pole, které obsahuje nativní ukazatel. Ukazatel deklaruje, kolik bajtů adresuje jako takový System.Span<T> , takže obsahující typ udržuje samotné informace:

// Preview
public class NativeBuffer
{
    /// <safety>
    /// Null, or points to a buffer of Length bytes.
    /// </safety>
    private unsafe byte* _pointer;

    public int Length { get; }

    public byte ReadAt(int index)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(index);
        ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Length);
        unsafe
        {
            // SAFETY: the bounds checks confine the read to the buffer that _pointer addresses.
            return _pointer[index];
        }
    }
}

readonly unsafe Pole spáruje kontrakt s integrovanou stráží: unsafe pojmenuje invariant a readonly zabraňuje zápisu, který by ho mohl po konstrukci přerušit. Označení vlastnosti nebo události unsafe nezpůsobí, že je volající v backingovém poli nebezpečný. Ve struktuře [StructLayout(LayoutKind.Explicit)]označíte každé pole nebo safeunsafe.

Bezpečné klíčové slovo

Aktualizovaný model přidá safe kontextové klíčové slovo, které ověřuje deklaraci je zvuk, kde kompilátor vyžaduje, abyste zvolili explicitně.

Člen extern volá nativní kód, takže kompilátor nemůže klasifikovat jeho bezpečnost. V rámci aktualizovaného modelu označíte každou extern deklaraci, včetně LibraryImport částečné metody, nebo safeunsafe:

// Preview
[LibraryImport("libc")]
internal static safe partial int getpid();

[LibraryImport("libc", StringMarshalling = StringMarshalling.Utf8)]
internal static unsafe partial nint strlen(byte* str);

getpid nepřijímá žádné parametry a vrací primitiva, takže autor ověřuje, že volání je bezpečné a volající ho používají bez obřadu. strlen přebírá nezpracovaný ukazatel, který nativní kód dereferences, takže deklarace je unsafe a šíří povinnost volajícím. Vynechání obou modifikátorů je chyba, která vás přinutí učinit bezpečnostní rozhodnutí. Pole ve struktuře s explicitním rozložením používá stejné pravidlo.

Chování souhlasu a křížového sestavení

Aktualizovaný model má dva nezávislé přepínače na úrovni projektu:

  • Nová vlastnost výslovného souhlasu zapne aktualizovaná pravidla. Když je vlastnost vypnutá, použijí se původní pravidla. Když je zapnutá, unsafe člen se rozšíří do volajících a kompilátor zaznamená volbu v sestavení s atributem MemorySafetyRulesAttribute .
  • Stávající AllowUnsafeBlocks vlastnost brány každý vzhled klíčového unsafe slova, včetně vnitřních bloků na call lokality. Výchozí hodnota je false, takže projekt ve výchozím nastavení nemůže volat žádné nebezpečné rozhraní API.

Tyto dvě vlastnosti se sloučují takto:

Vlastnost opt-in AllowUnsafeBlocks Result
Zapnuto Vypnuto (výchozí) Nejbezpečnější konfigurace. Projekt používá aktualizovaný model a neumožňuje žádný nebezpečný kód.
Zapnuto Zapnuto Projekt používá aktualizovaný model a umožňuje nebezpečný kód.
Off Off Původní model platí a projekt nemůže používat typy ukazatelů.
Off Zapnuto Použije se původní model a projekt může používat typy ukazatelů.

To, jestli jedno sestavení vynucuje aktualizovaná pravidla proti jinému, závisí na tom, ve které straně se přihlašuje:

  • Aktualizovaný volající modelu, aktualizovaný volaný model: Značky volaných unsafe procházejí metadaty. Volající zabalí každý hovor volajícímu nebezpečnému členu unsafe v bloku.
  • Aktualizovaný volající, volaný původní model: Režim kompatibility považuje všechny volané členy s typem ukazatele v podpisu jako nebezpečné volajícího, takže web volání potřebuje uzavřený unsafe blok. Tento režim udržuje rozhraní API založené na ukazatelích bezobslužné ztráty svého unsafe požadavku.
  • Volající původního modelu, aktualizovaný volaný model: Původní pravidla ukazatele stále platí. Volající nebezpečný člen, který nemá v podpisu žádný typ ukazatele, se dá volat z bezpečného kódu, protože volající původního modelu nemůže přečíst nové značky.

Specifikace jazyka C#

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

Návrh aktualizovaného modelu bezpečnosti paměti naleznete ve specifikaci funkce bezpečnosti paměti.

Viz také