Partager via


Code, types de pointeurs et pointeurs de fonction non sécurisés

La plupart du code C# que vous écrivez est « code vérifiable sécurisé ». code sécurisé vérifiable signifie que les outils .NET peuvent vérifier que le code est sécurisé. En règle générale, le code sécurisé n’accède pas directement à la mémoire à l’aide de pointeurs. Elle n’alloue pas non plus de mémoire brute. Il crée des objets managés à la place.

C# prend en charge un contexte unsafe, dans lequel vous pouvez écrire du code non vérifiable . Dans un contexte unsafe, le code peut utiliser des pointeurs, allouer et libérer des blocs de mémoire et appeler des méthodes à l’aide de pointeurs de fonction. Le code dangereux en C# n’est pas nécessairement dangereux ; c’est juste du code dont la sécurité ne peut pas être vérifiée.

Le code non sécurisé possède les propriétés suivantes :

  • Les méthodes, les types et les blocs de code peuvent être définis comme non sécurisés.
  • Dans certains cas, le code non sécurisé peut augmenter les performances d’une application en supprimant les vérifications des limites de tableau.
  • Le code non sécurisé est requis lorsque vous appelez des fonctions natives qui nécessitent des pointeurs.
  • L’utilisation de code non sécurisé introduit des risques de sécurité et de stabilité.
  • Le code qui contient des blocs non sécurisés doit être compilé avec l’option AllowUnsafeBlocks compilateur.

Types de pointeur

Dans un contexte non sécurisé, un type peut être un type de pointeur, en plus d’un type valeur ou d’un type référence. Une déclaration de type pointeur prend l’une des formes suivantes :

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

Le type spécifié avant le * dans un type de pointeur est appelé type de référence.

Les types de pointeur n’héritent pas de objet et aucune conversion n’existe entre les types de pointeur et object. Par ailleurs, le boxing et l'unboxing ne prennent pas en charge les pointeurs. Toutefois, vous pouvez convertir entre différents types de pointeur et entre les types de pointeur et les types intégraux.

Lorsque vous déclarez plusieurs pointeurs dans la même déclaration, vous écrivez l’astérisque (*) avec le type sous-jacent uniquement. Il n’est pas utilisé comme préfixe pour chaque nom de pointeur. Par exemple:

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

Le récupérateur de mémoire ne se préoccupe pas de savoir si un objet est pointé par des types pointeur. Si le référent est un objet dans le tas géré (y compris les variables locales capturées par des expressions lambda ou des délégués anonymes), l'objet doit être épinglé tant que le pointeur est utilisé.

La valeur de la variable de pointeur de type MyType* est l’adresse d’une variable de type MyType. Voici des exemples de déclarations de type pointeur :

  • int* p: p est un pointeur vers un entier.
  • int** p: p est un pointeur vers un pointeur vers un entier.
  • int*[] p: p est un tableau unidimensionnel de pointeurs vers des entiers.
  • char* p: p est un pointeur vers un char.
  • void* p: p est un pointeur vers un type inconnu.

L’opérateur d’indirection de pointeur * peut être utilisé pour accéder au contenu à l’emplacement vers lequel pointe la variable de pointeur. Par exemple, considérez la déclaration suivante :

int* myVariable;

L’expression *myVariable indique la variable int trouvée à l’adresse contenue dans myVariable.

Il existe plusieurs exemples de pointeurs dans les articles sur l’instruction fixed. L’exemple suivant utilise le mot clé unsafe et l’instruction fixed, et montre comment incrémenter un pointeur intérieur. Vous pouvez coller ce code dans la fonction Main d’une application console pour l’exécuter. Ces exemples doivent être compilés avec l'AllowUnsafeBlocks ensemble d’options du compilateur.

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

Vous ne pouvez pas appliquer l’opérateur indirection à un pointeur de type void*. Toutefois, vous pouvez utiliser un cast pour convertir un pointeur void en n'importe quel autre type pointeur, et inversement.

Un pointeur peut être null. L’application de l’opérateur indirection à un pointeur Null provoque un comportement défini par l’implémentation.

Le passage de pointeurs entre les méthodes peut entraîner un comportement non défini. Considérez une méthode qui retourne un pointeur vers une variable locale via un in, outou ref paramètre ou comme résultat de la fonction. Si le pointeur a été défini dans un bloc fixe, la variable à laquelle il pointe peut ne plus être fixe.

Le tableau suivant répertorie les opérateurs et les instructions qui peuvent fonctionner sur des pointeurs dans un contexte non sécurisé :

Opérateur/déclaration Utiliser
* Exécute l'indirection de pointeur.
-> Accède à un membre d’un struct par le biais d’un pointeur.
[] Indexe un pointeur.
& Obtient l’adresse d’une variable.
++ et -- Incrémente et décrémente les pointeurs.
+ et - Exécute des opérations arithmétiques sur les pointeurs.
==, !=, <, >, <=et >= Compare des pointeurs.
stackalloc Alloue de la mémoire sur la pile.
Instructions fixed Corrige temporairement une variable afin que son adresse soit trouvée.

Pour plus d’informations sur les opérateurs liés au pointeur, consultez opérateurs liés au pointeur.

Tout type de pointeur peut être converti implicitement en type void*. N’importe quel type de pointeur peut être affecté à la valeur null. Tout type de pointeur peut être converti explicitement en n’importe quel autre type de pointeur à l’aide d’une expression de cast. Vous pouvez également convertir n’importe quel type intégral en type pointeur, ou tout type de pointeur en type intégral. Ces conversions nécessitent un cast explicite.

L’exemple suivant convertit un int* en byte*. Notez que le pointeur pointe vers l’octet le plus bas adressé de la variable. Lorsque vous incrémentez successivement le résultat, jusqu’à la taille de int (4 octets), vous pouvez afficher les octets restants de la variable.

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

Mémoires tampons de taille fixe

Vous pouvez utiliser le mot clé fixed pour créer une mémoire tampon avec un tableau de taille fixe dans une structure de données. Les mémoires tampons de taille fixe sont utiles lorsque vous écrivez des méthodes qui interagissent avec des sources de données à partir d’autres langages ou plateformes. La mémoire tampon de taille fixe peut prendre tous les attributs ou modificateurs autorisés pour les membres de struct standard. La seule restriction est que le type de tableau doit être bool, byte, char, short, int, long, sbyte, ushort, uint, ulong, floatou double.

private fixed char name[30];

Dans le code sécurisé, un struct C# qui contient un tableau ne contient pas les éléments de tableau. Le struct contient une référence aux éléments à la place. Vous pouvez incorporer un tableau de taille fixe dans une struct lorsqu’il est utilisé dans un bloc de code non sécurisé.

La taille de l'struct suivante ne dépend pas du nombre d’éléments du tableau, car pathName est une référence :

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

Un struct peut contenir un tableau incorporé dans du code non sécurisé. Dans l’exemple suivant, le tableau fixedBuffer a une taille fixe. Vous utilisez une instruction fixed pour obtenir un pointeur vers le premier élément. Vous accédez aux éléments du tableau via ce pointeur. L’instruction fixed épingle le champ d’instance fixedBuffer à un emplacement spécifique en mémoire.

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

La taille du tableau de 128 éléments char est de 256 octets. Les mémoires tampons de caractères de taille fixe consomment toujours 2 octets par caractère, indépendamment de l'encodage. Cette taille de tableau reste identique même lorsque les mémoires tampons char sont marshalées vers des méthodes ou des structs d’API avec CharSet = CharSet.Auto ou CharSet = CharSet.Ansi. Pour plus d’informations, consultez CharSet.

L’exemple précédent illustre l’accès aux champs fixed sans épinglage. Un autre tableau de taille fixe courante est le tableau bool. Les éléments d’un tableau bool sont toujours de taille de 1 octet. Les tableaux bool ne conviennent pas pour créer des tableaux de bits ou des mémoires tampons.

Les mémoires tampons de taille fixe sont compilées avec System.Runtime.CompilerServices.UnsafeValueTypeAttribute qui indique au CLR qu’un type contient un tableau non managé susceptible de dépasser sa capacité. La mémoire allouée à l’aide de stackalloc active également automatiquement les fonctionnalités de détection de dépassement de mémoire tampon dans le CLR. L’exemple précédent montre comment une mémoire tampon de taille fixe peut exister dans un unsafe struct.

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

Le C# généré par le compilateur pour Buffer est attribué comme suit :

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

Les mémoires tampons fixes diffèrent des tableaux normaux des manières suivantes :

  • Ne peuvent être utilisées que dans un contexte unsafe.
  • Il ne peut s’agir que de champs d’instance de structs.
  • Ils sont toujours des vecteurs ou des tableaux unidimensionnels.
  • La déclaration doit inclure la longueur, comme fixed char id[8]. Vous ne pouvez pas utiliser fixed char id[].

Comment utiliser des pointeurs pour copier un tableau d’octets

L’exemple suivant utilise des pointeurs pour copier des octets d’un tableau vers un autre.

Cet exemple utilise le mot clé non sécurisé, qui vous permet d’utiliser des pointeurs dans la méthode Copy. L’instruction fixed permet de déclarer des pointeurs vers les tableaux source et de destination. L’instruction fixedfixe l’emplacement des tableaux source et de destination en mémoire afin que le ramasse-miettes ne déplace pas les tableaux. Les blocs de mémoire des tableaux sont libérés quand le bloc fixed est effectué. Étant donné que la méthode Copy de cet exemple utilise le mot clé unsafe, elle doit être compilée avec l’option AllowUnsafeBlocks compilateur.

Cet exemple montre comment accéder aux éléments des deux tableaux à l’aide d’index plutôt qu’un deuxième pointeur non managé. La déclaration des pointeurs pSource et pTarget épingle les tableaux.

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

Pointeurs de fonction

C# fournit des types delegate pour définir des objets pointeurs de fonction sécurisés. L’appel d’un délégué implique l’instanciation d’un type dérivé de System.Delegate et l’appel d’une méthode virtuelle à sa méthode Invoke. Cet appel virtuel utilise l’instruction IL callvirt. Dans les chemins de code critiques en matière de performances, l’utilisation de l’instruction IL calli est plus efficace.

Vous pouvez définir un pointeur de fonction à l’aide de la syntaxe delegate*. Le compilateur appelle la fonction à l’aide de l’instruction calli plutôt que d’instancier un objet delegate et d’appeler Invoke. Le code suivant déclare deux méthodes qui utilisent un delegate ou un delegate* pour combiner deux objets du même type. La première méthode utilise un type délégué System.Func<T1,T2,TResult>. La deuxième méthode utilise une déclaration delegate* avec les mêmes paramètres et le même type de retour :

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

Le code suivant montre comment déclarer une fonction locale statique et appeler la méthode UnsafeCombine à l’aide d’un pointeur vers cette fonction locale :

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

Le code précédent illustre plusieurs règles sur la fonction accessible en tant que pointeur de fonction :

  • Les pointeurs de fonction ne peuvent être déclarés que dans un contexte unsafe.
  • Les méthodes qui prennent un delegate* (ou retournent un delegate*) peuvent uniquement être appelées dans un contexte unsafe.
  • L’opérateur & pour obtenir l’adresse d’une fonction est autorisé uniquement sur static fonctions. (Cette règle s’applique aux fonctions membres et aux fonctions locales).

La syntaxe comporte des parallèles avec la déclaration de types delegate et l’utilisation de pointeurs. Le suffixe * sur delegate indique que la déclaration est un pointeur de fonction . L'& lors de l’affectation d’un groupe de méthodes à un pointeur de fonction indique que l’opération prend l’adresse de la méthode.

Vous pouvez spécifier la convention d’appel d’un delegate* à l’aide des mots clés managed et unmanaged. En outre, pour les pointeurs de fonction unmanaged, vous pouvez spécifier une convention d’appel. Les déclarations suivantes montrent des exemples de chacun d’eux. La première déclaration utilise la convention d’appel managed, qui est la valeur par défaut. Les quatre suivantes utilisent une convention d’appel unmanaged. Chacun spécifie l’une des conventions d’appel ECMA 335 : Cdecl, Stdcall, Fastcallou Thiscall. La dernière déclaration utilise la convention d’appel unmanaged, demandant au CLR de choisir la convention d’appel par défaut pour la plateforme. Le CLR choisit la convention d’appel au moment de l’exécution.

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

Vous pouvez en savoir plus sur les pointeurs de fonction dans la spécification de fonctionnalité du pointeur de fonction .

Spécification du langage C#

Pour plus d'informations, consultez le chapitre Code non sécurisé de la spécification du langage C# .