Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
La plupart du code C# que vous écrivez est un code vérifiable sécurisé. Code sécurisé de façon fiable signifie que .NET outils peuvent vérifier que le code est sécurisé. En général, 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.
Le langage C# documente la version la plus récente de la langue C#. Il contient également la documentation initiale des fonctionnalités dans les préversions publiques pour la prochaine version du langage.
La documentation identifie toute fonctionnalité introduite en premier dans les trois dernières versions de la langue ou dans les préversions publiques actuelles.
Conseil / Astuce
Pour savoir quand une fonctionnalité a été introduite en C#, consultez l’article sur l’historique des versions du langage C#.
C# prend également en charge un unsafe contexte dans lequel vous pouvez écrire du code non vérifiable . Le code dangereux n'est pas nécessairement dangereux ; il s'agit simplement du code dont la sécurité ne peut pas être vérifiée par .NET outils. Vous utilisez du code non sécurisé pour appeler des fonctions natives qui nécessitent des pointeurs et, dans certains cas, pour améliorer les performances grâce à l’accès direct à la mémoire qui évite les vérifications liées aux tableaux. Le code non sécurisé introduit également des risques de sécurité et de stabilité. Pour compiler du code qui contient un unsafe contexte, ajoutez l’option du compilateur AllowUnsafeBlocks .
C# définit deux modèles pour ce qui compte comme code non sécurisé : le modèle d'origine et un modèle de sécurité de mémoire mis à jour en préversion en C# 15 et .NET 11. Pour plus d’informations sur la façon dont les deux modèles diffèrent, consultez Deux modèles pour le code non sécurisé.
Pour plus d’informations sur les meilleures pratiques relatives au code non sécurisé en C#, consultez les meilleures pratiques en matière de code non sécurisé.
Deux modèles pour le code non sécurisé
C# définit deux modèles pour le code non sécurisé. Le modèle détermine en effet quelles opérations nécessitent un unsafe contexte et comment le unsafe modificateur sur un membre affecte les appelants.
-
Modèle non sécurisé d’origine : le
unsafecontexte couvre l’existence des fonctionnalités de pointeur. Vous déclarez un type de pointeur, prenez l’adresse d’une variable, déréférencez un pointeur, convertissez unestackallocexpression en pointeur ou appliquez-lasizeofà un type arbitraire uniquement à l’intérieur d’ununsafecontexte. (Unestackallocexpression affectée à unSpan<T>code sécurisé ouReadOnlySpan<T>est autorisée dans le code sécurisé.) Leunsafemodificateur d’un type, d’un membre ou d’un bloc établit ce contexte, mais n’impose aucune obligation aux appelants. C# 1.0 a introduit ce modèle et reste la valeur par défaut. -
Modèle de sécurité de la mémoire mis à jour : le
unsafecontexte couvre les opérations qui accèdent à la mémoire que le runtime ne gère pas. L’existence d’un pointeur n’est pas dangereuse ; la déréférence d’un pointeur est. Leunsafemodificateur d’un membre devient un contrat qui propage l’obligation d’auditer la sécurité à l’appelant. Ce modèle est en préversion en C# 15 et .NET 11.
Le tableau suivant compare les opérations qui nécessitent un unsafe contexte dans chaque modèle.
| Operation | Modèle d’origine | Modèle mis à jour |
|---|---|---|
Déclarer un type de pointeur ou prendre une adresse avec & |
Exige unsafe |
Autorisé dans le code sécurisé |
L’instruction fixed. |
Exige unsafe |
Autorisé dans le code sécurisé |
Convertir une stackalloc expression en pointeur |
Exige unsafe |
Autorisé dans le code sécurisé |
Opérateur sizeof sur n’importe quel type non managé |
Exige unsafe |
Autorisé dans le code sécurisé |
Indirection de pointeur (*p), accès aux membres (p->m) ou accès aux éléments (p[i]) |
Exige unsafe |
Exige unsafe |
| Appel de pointeur de fonction | Exige unsafe |
Exige unsafe |
| Accès aux éléments sur une mémoire tampon de taille fixe | Exige unsafe |
Exige unsafe |
Appeler un membre marqué unsafe |
Aucune exigence de l’appelant | Exige unsafe |
Pour essayer le modèle mis à jour, utilisez le sdk .NET 11 (en préversion) et définissez l’option previewdu LangVersion compilateur sur . Les relaxations de pointeur s’appliquent chaque fois que vous compilez avec le compilateur C# 15 et la version du preview langage. L’application complète, y compris les obligations de l’appelant et l’adhésion à l’assembly, est toujours en cours de développement. Pour plus d’informations, consultez le modèle de sécurité de la mémoire mis à jour (préversion).
Modèle non sécurisé d’origine
Dans le modèle d’origine, le unsafe mot clé établit un contexte non sécurisé sur un type, un membre ou un bloc, et ce contexte déverrouille les fonctionnalités de pointeur décrites dans les sections suivantes. Le unsafe modificateur modifie uniquement ce que le code marqué peut faire ; il n’impose aucune exigence aux appelants. Pour compiler l’un de ces exemples, définissez l’option du compilateur AllowUnsafeBlocks .
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 que vous spécifiez avant le * type de pointeur dans un type de pointeur est le type de référence.
Les types de pointeur n’héritent pas de l’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, é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érentiel est un objet dans le tas managé (y compris les variables locales capturées par des expressions lambda ou des délégués anonymes), vous devez épingler l’objet 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:pest un pointeur vers un entier. -
int** p:pest un pointeur vers un pointeur vers un entier. -
int*[] p:pest un tableau unidimensionnel de pointeurs vers des entiers. -
char* p:pest un pointeur vers un char. -
void* p:pest un pointeur vers un type inconnu.
Vous pouvez utiliser l’opérateur * d’indirection de pointeur 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. Vous pouvez convertir explicitement n’importe quel type de pointeur 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: {number}");
/* Output:
The 4 bytes of the integer: 00 04 00 00
The value of the integer: 1024
*/
}
Mémoires tampons de taille fixe
Les tableaux sont des types de référence. Dans le code sécurisé, un champ de struct qui est un tableau stocke uniquement une référence aux éléments du tableau, et non les éléments eux-mêmes. La taille des éléments suivants struct ne dépend pas du nombre d’éléments du tableau, car pathName il s’agit d’une référence :
public struct PathArray
{
public char[] pathName;
private int reserved;
}
Pour stocker le contenu du tableau à l’intérieur du struct lui-même, utilisez le fixed mot clé pour déclarer une mémoire tampon de taille fixe. Le fixed mot clé nécessite un unsafe contexte. 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. Une 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, , shortbytechar, int, long, sbyte, , ushort, ulonguint, , float, ou double:
private fixed char name[30];
Dans l’exemple suivant, le tableau fixedBuffer a une taille fixe. Vous utilisez une fixed instruction pour obtenir un pointeur vers le premier élément, puis accéder aux éléments du tableau via ce pointeur. L’instruction fixed épingle le fixedBuffer champ d’instance à 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 :
- Vous ne pouvez les utiliser que dans un
unsafecontexte. - Ils ne peuvent être que des champs d’instance de structs.
- Ils sont toujours des vecteurs ou des tableaux unidimensionnels.
- La déclaration doit inclure la longueur, par
fixed char id[8]exemple . Vous ne pouvez pas utiliserfixed char id[].
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 delegate* syntaxe. Le compilateur appelle la fonction à l’aide de l’instruction calli plutôt que d’instancier un delegate objet 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 à l’aide UnsafeCombine 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 :
- Vous ne pouvez déclarer que des pointeurs de fonction dans un
unsafecontexte. - Vous pouvez uniquement appeler des méthodes qui prennent un
delegate*(ou retournent undelegate*) dans ununsafecontexte. - L’opérateur
&pour obtenir l’adresse d’une fonction est autorisé uniquement surstaticfonctions. 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 section Pointeurs de fonction de la spécification du langage C#.
Exemple : 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 unsafe mot clé, qui vous permet d’utiliser des pointeurs dans la Copy méthode. L’instruction fixed déclare 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. Le fixed bloc épingle les blocs de mémoire pour les tableaux dans l’étendue du bloc. Étant donné que la Copy méthode de cet exemple utilise le mot clé, vous devez la unsafe compiler à l’aide de l’option du compilateur AllowUnsafeBlocks .
Cet exemple accède 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
*/
}
Modèle de sécurité de la mémoire mis à jour (préversion)
Important
Le modèle de sécurité de la mémoire mis à jour est une fonctionnalité en préversion en C# 15 et .NET 11. Il continue d’évoluer en fonction des commentaires au cours des versions préliminaires. Pour essayer le modèle, utilisez le SDK .NET 11 (préversion) et définissez l’option previewdu LangVersion compilateur sur . Le compilateur dans .NET 11 Preview 5 implémente les relaxations de pointeur, mais n'applique pas encore les obligations de l'appelant, l'assembly opt-in ou le safe mot clé. Pour obtenir la conception complète, consultez la spécification de la fonctionnalité de sécurité de la mémoire.
Le modèle mis à jour sépare deux éléments que le modèle d’origine traite comme un : l’existence de code de pointeur et la propagation des obligations de sécurité aux appelants. Le marquage d’un membre unsafe n’autorise plus seulement les pointeurs dans son corps ; il rend l’appelant membre non sécurisé, de sorte que chaque appelant doit propager cette obligation ou la décharger derrière une limite valide et pouvant être appelée en toute sécurité. Pour prendre en charge cette séparation, le modèle réduit également le contexte non sécurisé : l’existence d’un pointeur n’est pas dangereuse, seules les opérations qui accèdent à la mémoire que le runtime ne gère pas. Le rétrécissement vous permet de contenir, de passer et de retourner des pointeurs dans du code sécurisé, tout en unsafe marque les opérations et les membres qui peuvent réellement violer la sécurité de la mémoire.
Membres non sécurisés de l’appelant
Dans le modèle d’origine, le unsafe modificateur sur un membre autorise uniquement les pointeurs dans la signature et le corps du membre. Il n’informe pas les appelants de la sécurité. Le modèle mis à jour donne la signification du modificateur pour les appelants. Lorsque vous marquez un membre unsafe, le compilateur le traite comme un appelant non sécurisé (également appelé « nécessite-unsafe ») : chaque appelant doit l’appeler à partir d’un unsafe contexte et l’obligation d’auditer la sécurité passe à cet appelant.
Le unsafe modificateur sur une signature membre n’établit plus de contexte non sécurisé pour le corps. Les deux rôles sont divisés :
- Le
unsafemodificateur sur la signature propage l’obligation pour les appelants. - Un bloc interne
unsafeétend les opérations qui accèdent à la mémoire non managée.
Dans la maquette d’aperçu suivante, ReadInt32 l’appelant est non sécurisé. La signature porte le unsafe modificateur et un bloc interne unsafe encapsule la déréférence :
// 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;
}
}
Un appelant encapsule l’appel dans son propre unsafe bloc :
// Preview
unsafe
{
int value = ReadInt32(buffer);
}
Le modèle mis à jour renforce également quelques règles connexes :
- Le
unsafemodificateur génère une erreur sur une déclaration de type, un constructeur statique et un finaliseur, car le modificateur n’a pas d’appelant à informer. - Les délégués ne peuvent pas être
unsafe, car un délégué est en forme de type. - Type dont le constructeur sans paramètre n’est
unsafepas conforme à lanew()contrainte.
Opérations nécessitant un contexte non sécurisé
Les opérations qui accèdent à la mémoire pointée nécessitent un unsafe contexte :
- Indirection de pointeur (
*p), accès aux membres du pointeur (p->member) et accès aux éléments de pointeur (p[i]). - Appel de pointeur de fonction.
- Accès aux éléments sur une mémoire tampon de taille fixe.
L’exemple suivant épingle un tableau sans unsafe contexte, mais déréférence le pointeur à l’intérieur d’un :
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;
}
}
}
Opérations détendues
Les opérations qui n’accèdent pas à la mémoire pointée ne nécessitent plus de unsafe contexte :
- Déclaration d’un type de pointeur et prise de l’adresse d’une variable avec l’opérateur
&. - Instruction
fixedqui épingle une variable. - Conversion d’une
stackallocexpression en pointeur. - Opérateur
sizeofappliqué à n’importe quel type non managé.
L’exemple suivant crée et épingle des pointeurs sans unsafe contexte :
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;
}
}
Ces relaxations s’appliquent chaque fois que vous compilez avec la preview version de langue, qu’un assembly opte ou non pour les règles de sécurité de la mémoire mises à jour.
Obligations non sécurisées de l’appelant de décharge
Un membre qui appelle une opération appelante non sécurisée a deux choix : propager l’obligation ou la décharger.
-
Propager : Marquer votre propre membre
unsafe. L’obligation passe à vos appelants. Utilisez la propagation lorsque vous ne pouvez pas valider entièrement l’obligation vous-même. -
Décharge : laissez la signature de votre membre en sécurité. Validez l’obligation à l’intérieur du membre, généralement avec des gardes d’exécution, puis effectuez l’opération non sécurisée dans un bloc interne
unsafe. Un membre qui contient un bloc interneunsafe, mais qui ne marque pas sa propre signatureunsafeest une limite non sécurisée : il transforme le code non sécurisé en surface pouvant être appelée en sécurité.
La maquette de la préversion suivante valide son entrée avec un garde, épingle un tableau managé et lit le pointeur. Les appelants n’ont pas besoin d’un unsafe contexte, car la méthode décharge l’obligation :
// 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;
}
}
}
La vérification null et la longueur du tableau excluent les entrées qui laisseraient une lecture s’exécuter au-delà de la mémoire tampon, de sorte que la déréférence à l’intérieur du unsafe bloc est son. La méthode ne laisse aucune obligation résiduelle, elle expose donc une signature pouvant être appelée en toute sécurité.
Documentation sur la sécurité
Un membre appelant non sécurisé doit documenter ce que l’appelant doit garantir. Le modèle mis à jour encourage deux styles de commentaires complémentaires :
- Un
/// <safety>bloc de documentation au-dessus de la signature indique le contrat formel : les conditions qu’un appelant doit satisfaire. Un analyseur peut marquer un membre appelant non sécurisé qui en manque un. - Un
// SAFETY:commentaire à l’intérieur d’ununsafebloc enregistre pourquoi l’opération est sonore à cet endroit, pour les développeurs et les auditeurs qui lisent le corps.
La maquette d’aperçu suivante montre les deux styles sur une méthode non sécurisée ReadByte de l’appelant :
// 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];
}
}
Le /// <safety> bloc vous indique le contrat. Le contrat appartient à la documentation où chaque appelant et réviseur le voit.
Champs non sécurisés
Utilisez le unsafe modificateur d’un champ lorsque son type déclaré n’exprime pas les contrats que le type englobant gère et que d’autres codes dépendent. La non-sécurité existe dans l’écart entre ce que le système de type voit et ce que le type promet. Le modificateur force chaque écriture dans le champ dans un unsafe bloc, ce qui conserve les écritures pouvant être examinées à un seul endroit.
Le cas le plus clair est un champ qui contient un pointeur natif. Le pointeur ne déclare pas le nombre d’octets qu’il traite, System.Span<T> de sorte que le type conteneur conserve ces informations elles-mêmes :
// 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];
}
}
}
Un readonly unsafe champ associe le contrat à un garde intégré : unsafe nomme l’invariant et readonly empêche une écriture qui peut l’interrompre après la construction. Le marquage d’une propriété ou d’un événement unsafe ne rend pas son appelant de champ de stockage non sécurisé. Dans un struct avec [StructLayout(LayoutKind.Explicit)], vous marquez chaque champ ou safeunsafe.
Mot clé safe
Le modèle mis à jour ajoute un safe mot clé contextuel qui atteste qu’une déclaration est son, où le compilateur vous oblige à faire le choix explicite.
Un extern membre appelle du code natif afin que le compilateur ne puisse pas classifier sa sécurité. Sous le modèle mis à jour, vous marquez chaque extern déclaration, y compris une LibraryImport méthode partielle, soit safeunsafe:
// Preview
[LibraryImport("libc")]
internal static safe partial int getpid();
[LibraryImport("libc", StringMarshalling = StringMarshalling.Utf8)]
internal static unsafe partial nint strlen(byte* str);
getpid n’accepte aucun paramètre et retourne une primitive, de sorte que l’auteur atteste que l’appel est sûr et que les appelants l’utilisent sans cérémonie.
strlen prend un pointeur brut que le code natif déréférence, de sorte que la déclaration est unsafe et propage l’obligation aux appelants. Omettre les deux modificateurs est une erreur, ce qui vous oblige à prendre la décision de sécurité. Un champ d’un struct avec disposition explicite utilise la même règle.
Comportement d’inscription et d’assembly croisé
Le modèle mis à jour a deux commutateurs indépendants au niveau du projet :
- Une nouvelle propriété d’inscription active les règles mises à jour. Lorsque la propriété est désactivée, les règles d’origine s’appliquent. Lorsqu’il est activé,
unsafesur un membre se propage aux appelants, et le compilateur enregistre le choix dans l’assembly avec l’attribut MemorySafetyRulesAttribute . - La propriété AllowUnsafeBlocks existante porte toutes les apparences du
unsafemot clé, y compris les blocs internes sur les sites d’appel. Par défaut, un projet par défautfalsene peut pas appeler d’API non sécurisée.
Les deux propriétés se combinent comme suit :
| Propriété d’inscription | AllowUnsafeBlocks |
Result |
|---|---|---|
| Activé | Désactivé (valeur par défaut) | Configuration la plus sûre. Le projet utilise le modèle mis à jour et n’autorise aucun code non sécurisé. |
| Activé | Activé | Le projet utilise le modèle mis à jour et autorise le code non sécurisé. |
| Désactivé | Désactivé | Le modèle d’origine s’applique et le projet ne peut pas utiliser de types de pointeur. |
| Désactivé | Activé | Le modèle d’origine s’applique et le projet peut utiliser des types de pointeur. |
Si un assembly applique les règles mises à jour contre une autre dépend de l’option de choix du côté :
-
Appelant de modèle mis à jour, appelé de modèle mis à jour : les marqueurs de l’appelé
unsafetransitent par les métadonnées. L’appelant encapsule chaque appel à un membre non sécurisé de l’appelant dans ununsafebloc. -
Appelant de modèle mis à jour, appelé de modèle d’origine : un mode de compatibilité traite tout membre appelé avec un type de pointeur dans sa signature comme non sécurisé pour l’appelant, de sorte que le site d’appel a besoin d’un bloc englobant
unsafe. Ce mode empêche une API basée sur un pointeur de perdre silencieusement sesunsafebesoins. - Appelant de modèle d’origine, appelé de modèle mis à jour : les règles de pointeur d’origine s’appliquent toujours. Un membre appelant non sécurisé qui n’a aucun type de pointeur dans sa signature devient appelant à partir du code sécurisé, car l’appelant du modèle d’origine ne peut pas lire les nouveaux marqueurs.
Spécification du langage C#
Pour plus d'informations, consultez le chapitre Code non sécurisé de la spécification du langage C# .
Pour la conception du modèle de sécurité de mémoire mis à jour, consultez la spécification de la fonctionnalité de sécurité de la mémoire.