Présentation de SAL
Le langage d’annotation de code source (SAL) Microsoft fournit un ensemble d’annotations que vous pouvez utiliser pour décrire comment une fonction utilise ses paramètres, les hypothèses qu’elle fait à leur sujet et les garanties qu’elle apporte quand elle se termine. Les annotations sont définies dans le fichier d’en-tête <sal.h>
. L’analyse de code Visual Studio pour C++ utilise les annotations SAL pour modifier son analyse des fonctions. Pour plus d’informations sur sal 2.0 pour le développement de pilotes Windows, consultez les annotations SAL 2.0 pour les pilotes Windows.
En mode natif, C et C++ fournissent uniquement des moyens limités pour les développeurs d’exprimer constamment l’intention et l’invariance. En utilisant des annotations SAL, vous pouvez décrire vos fonctions plus en détail afin que les développeurs qui les consomment puissent mieux comprendre comment les utiliser.
Qu’est-ce que SAL et pourquoi devez-vous l’utiliser ?
Tout simplement indiqué, SAL est un moyen peu coûteux de laisser le compilateur vérifier votre code pour vous.
SAL rend le code plus précieux
SAL peut vous aider à rendre votre conception de code plus compréhensible, tant pour les humains que pour les outils d’analyse du code. Prenons cet exemple qui montre la fonction memcpy
runtime C :
void * memcpy(
void *dest,
const void *src,
size_t count
);
Pouvez-vous dire ce que fait cette fonction ? Lorsqu’une fonction est implémentée ou appelée, certaines propriétés doivent être conservées pour garantir l’exactitude du programme. En examinant une déclaration telle que celle de l’exemple, vous ne savez pas ce qu’elles sont. Sans annotations SAL, vous devrez vous appuyer sur la documentation ou les commentaires de code. Voici ce que la documentation pour memcpy
dire :
"
memcpy
copie le nombre d’octets de src à dest ;wmemcpy
copie le nombre de caractères larges (deux octets). Si la source et la destination se chevauchent, le comportement dememcpy
n'est pas défini. Utilisezmemmove
pour gérer les régions qui se chevauchent.
Important : assurez-vous que la mémoire tampon de destination est de la même taille ou supérieure à la mémoire tampon source. Pour plus d’informations, consultez Éviter les dépassements de mémoire tampon. »
La documentation contient quelques bits d’informations qui suggèrent que votre code doit conserver certaines propriétés pour garantir l’exactitude du programme :
memcpy
copie lescount
octets de la mémoire tampon source vers la mémoire tampon de destination.La mémoire tampon de destination doit être au moins aussi grande que la mémoire tampon source.
Toutefois, le compilateur ne peut pas lire la documentation ni les commentaires informels. Il ne sait pas qu’il existe une relation entre les deux mémoires tampons et count
, et il ne peut pas non plus deviner efficacement une relation. SAL peut fournir plus de clarté sur les propriétés et l’implémentation de la fonction, comme illustré ici :
void * memcpy(
_Out_writes_bytes_all_(count) void *dest,
_In_reads_bytes_(count) const void *src,
size_t count
);
Notez que ces annotations ressemblent aux informations de la documentation, mais elles sont plus concises et suivent un modèle sémantique. Lorsque vous lisez ce code, vous pouvez rapidement comprendre les propriétés de cette fonction et comment éviter les problèmes de sécurité de dépassement de mémoire tampon. Mieux encore, les modèles sémantiques fournis par SAL peuvent améliorer l’efficacité et l’efficacité des outils d’analyse de code automatisé lors de la découverte précoce des bogues potentiels. Imaginez que quelqu’un écrit cette implémentation buggy de wmemcpy
:
wchar_t * wmemcpy(
_Out_writes_all_(count) wchar_t *dest,
_In_reads_(count) const wchar_t *src,
size_t count)
{
size_t i;
for (i = 0; i <= count; i++) { // BUG: off-by-one error
dest[i] = src[i];
}
return dest;
}
Cette implémentation contient une erreur courante off-by-one. Heureusement, l’auteur du code incluait l’annotation de taille de mémoire tampon SAL : un outil d’analyse du code pouvait intercepter le bogue en analysant cette fonction seule.
Notions de base de SAL
SAL définit quatre types de paramètres de base, classés par modèle d’utilisation.
Catégorie | Annotation de paramètre | Description |
---|---|---|
Entrée à la fonction appelée | _In_ |
Les données sont transmises à la fonction appelée et sont traitées en lecture seule. |
Entrée à la fonction appelée et sortie à l’appelant | _Inout_ |
Les données utilisables sont transmises à la fonction et potentiellement modifiées. |
Sortie vers l’appelant | _Out_ |
L’appelant fournit uniquement de l’espace pour que la fonction appelée écrive. La fonction appelée écrit des données dans cet espace. |
Sortie du pointeur vers l’appelant | _Outptr_ |
Comme la sortie pour l’appelant. La valeur retournée par la fonction appelée est un pointeur. |
Ces quatre annotations de base peuvent être rendues plus explicites de différentes façons. Par défaut, les paramètres de pointeur annotés sont supposés être requis. Ils doivent être non NULL pour que la fonction réussisse. La variante la plus couramment utilisée des annotations de base indique qu’un paramètre de pointeur est facultatif, s’il est NULL, la fonction peut toujours réussir à effectuer son travail.
Ce tableau montre comment faire la distinction entre les paramètres obligatoires et facultatifs :
Les paramètres sont requis | Les paramètres sont facultatifs | |
---|---|---|
Entrée à la fonction appelée | _In_ |
_In_opt_ |
Entrée à la fonction appelée et sortie à l’appelant | _Inout_ |
_Inout_opt_ |
Sortie vers l’appelant | _Out_ |
_Out_opt_ |
Sortie du pointeur vers l’appelant | _Outptr_ |
_Outptr_opt_ |
Ces annotations permettent d’identifier les valeurs non initialisées possibles et le pointeur Null non valide utilise de manière formelle et précise. La transmission de LA valeur NULL à un paramètre requis peut entraîner un blocage, ou un code d’erreur « échec » peut être retourné. De l’une ou l’autre manière, la fonction ne peut pas réussir à accomplir son travail.
Exemples SAL
Cette section présente des exemples de code pour les annotations SAL de base.
Utilisation de l’outil d’analyse visual Studio Code pour rechercher des défauts
Dans les exemples, l’outil Visual Studio Code Analysis est utilisé avec des annotations SAL pour rechercher des défauts de code. Voici comment procéder.
Pour utiliser les outils d’analyse de code Visual Studio et sal
Dans Visual Studio, ouvrez un projet C++ qui contient des annotations SAL.
Dans la barre de menus, choisissez Générer, Exécuter l’analyse du code sur la solution.
Considérez l’exemple _In_ dans cette section. Si vous exécutez l’analyse du code dessus, cet avertissement s’affiche :
La valeur de paramètre non valide C6387 'pInt' peut être '0' : cela ne respecte pas la spécification de la fonction 'InCallee'.
Exemple : annotation _In_
L’annotation _In_
indique que :
Le paramètre doit être valide et ne sera pas modifié.
La fonction lit uniquement à partir de la mémoire tampon à élément unique.
L’appelant doit fournir la mémoire tampon et l’initialiser.
_In_
spécifie « en lecture seule ». Une erreur courante consiste à s’appliquer_In_
à un paramètre qui doit avoir l’annotation à la_Inout_
place._In_
est autorisé mais ignoré par l’analyseur sur les scalaires non pointeurs.
void InCallee(_In_ int *pInt)
{
int i = *pInt;
}
void GoodInCaller()
{
int *pInt = new int;
*pInt = 5;
InCallee(pInt);
delete pInt;
}
void BadInCaller()
{
int *pInt = NULL;
InCallee(pInt); // pInt should not be NULL
}
Si vous utilisez Visual Studio Code Analysis sur cet exemple, il valide que les appelants passent un pointeur non Null à une mémoire tampon initialisée pour pInt
. Dans ce cas, pInt
le pointeur ne peut pas être NULL.
Exemple : annotation _In_opt_
_In_opt_
est identique à _In_
, sauf que le paramètre d’entrée est autorisé à être NULL et, par conséquent, la fonction doit vérifier cela.
void GoodInOptCallee(_In_opt_ int *pInt)
{
if(pInt != NULL) {
int i = *pInt;
}
}
void BadInOptCallee(_In_opt_ int *pInt)
{
int i = *pInt; // Dereferencing NULL pointer 'pInt'
}
void InOptCaller()
{
int *pInt = NULL;
GoodInOptCallee(pInt);
BadInOptCallee(pInt);
}
Visual Studio Code Analysis vérifie que la fonction vérifie la valeur NULL avant d’accéder à la mémoire tampon.
Exemple : annotation _Out_
_Out_
prend en charge un scénario courant dans lequel un pointeur non NULL pointant vers une mémoire tampon d’élément est passé et la fonction initialise l’élément. L’appelant n’a pas besoin d’initialiser la mémoire tampon avant l’appel ; la fonction appelée promet de l’initialiser avant de retourner.
void GoodOutCallee(_Out_ int *pInt)
{
*pInt = 5;
}
void BadOutCallee(_Out_ int *pInt)
{
// Did not initialize pInt buffer before returning!
}
void OutCaller()
{
int *pInt = new int;
GoodOutCallee(pInt);
BadOutCallee(pInt);
delete pInt;
}
Visual Studio Code Analysis Tool vérifie que l’appelant transmet un pointeur non NULL à une mémoire tampon et pInt
que la mémoire tampon est initialisée par la fonction avant de retourner.
Exemple : annotation _Out_opt_
_Out_opt_
est identique à _Out_
, sauf que le paramètre est autorisé à être NULL et, par conséquent, la fonction doit vérifier cela.
void GoodOutOptCallee(_Out_opt_ int *pInt)
{
if (pInt != NULL) {
*pInt = 5;
}
}
void BadOutOptCallee(_Out_opt_ int *pInt)
{
*pInt = 5; // Dereferencing NULL pointer 'pInt'
}
void OutOptCaller()
{
int *pInt = NULL;
GoodOutOptCallee(pInt);
BadOutOptCallee(pInt);
}
Visual Studio Code Analysis valide que cette fonction vérifie la valeur NULL avant pInt
qu’elle ne soit déréférée et, si pInt
elle n’est pas NULL, que la mémoire tampon est initialisée par la fonction avant de retourner.
Exemple : annotation _Inout_
_Inout_
est utilisé pour annoter un paramètre de pointeur qui peut être modifié par la fonction. Le pointeur doit pointer vers des données initialisées valides avant l’appel, et même s’il change, il doit toujours avoir une valeur valide lors du retour. L’annotation spécifie que la fonction peut lire et écrire librement dans la mémoire tampon d’un élément. L’appelant doit fournir la mémoire tampon et l’initialiser.
Remarque
Comme _Out_
, _Inout_
doit s’appliquer à une valeur modifiable.
void InOutCallee(_Inout_ int *pInt)
{
int i = *pInt;
*pInt = 6;
}
void InOutCaller()
{
int *pInt = new int;
*pInt = 5;
InOutCallee(pInt);
delete pInt;
}
void BadInOutCaller()
{
int *pInt = NULL;
InOutCallee(pInt); // 'pInt' should not be NULL
}
Visual Studio Code Analysis valide que les appelants passent un pointeur non NULL à une mémoire tampon initialisée pour pInt
, et que, avant le retour, pInt
n’est toujours pas NULL et que la mémoire tampon est initialisée.
Exemple : annotation _Inout_opt_
_Inout_opt_
est identique à _Inout_
, sauf que le paramètre d’entrée est autorisé à être NULL et, par conséquent, la fonction doit vérifier cela.
void GoodInOutOptCallee(_Inout_opt_ int *pInt)
{
if(pInt != NULL) {
int i = *pInt;
*pInt = 6;
}
}
void BadInOutOptCallee(_Inout_opt_ int *pInt)
{
int i = *pInt; // Dereferencing NULL pointer 'pInt'
*pInt = 6;
}
void InOutOptCaller()
{
int *pInt = NULL;
GoodInOutOptCallee(pInt);
BadInOutOptCallee(pInt);
}
Visual Studio Code Analysis vérifie que cette fonction vérifie la valeur NULL avant d’accéder à la mémoire tampon et, si pInt
elle n’est pas NULL, que la mémoire tampon est initialisée par la fonction avant de retourner.
Exemple : annotation _Outptr_
_Outptr_
est utilisé pour annoter un paramètre destiné à retourner un pointeur. Le paramètre lui-même ne doit pas être NULL, et la fonction appelée retourne un pointeur non NULL dans celui-ci et ce pointeur pointe vers des données initialisées.
void GoodOutPtrCallee(_Outptr_ int **pInt)
{
int *pInt2 = new int;
*pInt2 = 5;
*pInt = pInt2;
}
void BadOutPtrCallee(_Outptr_ int **pInt)
{
int *pInt2 = new int;
// Did not initialize pInt buffer before returning!
*pInt = pInt2;
}
void OutPtrCaller()
{
int *pInt = NULL;
GoodOutPtrCallee(&pInt);
BadOutPtrCallee(&pInt);
}
Visual Studio Code Analysis valide que l’appelant passe un pointeur non NULL pour *pInt
, et que la mémoire tampon est initialisée par la fonction avant de retourner.
Exemple : annotation _Outptr_opt_
_Outptr_opt_
est identique à _Outptr_
, sauf que le paramètre est facultatif : l’appelant peut passer un pointeur NULL pour le paramètre.
void GoodOutPtrOptCallee(_Outptr_opt_ int **pInt)
{
int *pInt2 = new int;
*pInt2 = 6;
if(pInt != NULL) {
*pInt = pInt2;
}
}
void BadOutPtrOptCallee(_Outptr_opt_ int **pInt)
{
int *pInt2 = new int;
*pInt2 = 6;
*pInt = pInt2; // Dereferencing NULL pointer 'pInt'
}
void OutPtrOptCaller()
{
int **ppInt = NULL;
GoodOutPtrOptCallee(ppInt);
BadOutPtrOptCallee(ppInt);
}
Visual Studio Code Analysis valide que cette fonction vérifie la valeur NULL avant *pInt
d’être déréférée et que la mémoire tampon est initialisée par la fonction avant de retourner.
Exemple : Annotation _Success_ en combinaison avec _Out_
Les annotations peuvent être appliquées à la plupart des objets. En particulier, vous pouvez annoter une fonction entière. L’une des caractéristiques les plus évidentes d’une fonction est qu’elle peut réussir ou échouer. Mais comme l’association entre une mémoire tampon et sa taille, C/C++ ne peut pas exprimer la réussite ou l’échec des fonctions. En utilisant l’annotation _Success_
, vous pouvez dire à quoi ressemble la réussite d’une fonction. Le paramètre de l’annotation _Success_
est simplement une expression qui, lorsqu’elle est vraie, indique que la fonction a réussi. L’expression peut être tout ce que l’analyseur d’annotation peut gérer. Les effets des annotations après la retour de la fonction ne s’appliquent que lorsque la fonction réussit. Cet exemple montre comment _Success_
interagir avec _Out_
pour faire la bonne chose. Vous pouvez utiliser le mot clé return
pour représenter la valeur de retour.
_Success_(return != false) // Can also be stated as _Success_(return)
bool GetValue(_Out_ int *pInt, bool flag)
{
if(flag) {
*pInt = 5;
return true;
} else {
return false;
}
}
L’annotation _Out_
amène Visual Studio Code Analysis à valider que l’appelant transmet un pointeur non NULL à une mémoire tampon, pInt
et que la mémoire tampon est initialisée par la fonction avant de retourner.
Bonne pratique sal
Ajout d’annotations au code existant
SAL est une technologie puissante qui peut vous aider à améliorer la sécurité et la fiabilité de votre code. Après avoir appris SAL, vous pouvez appliquer la nouvelle compétence à votre travail quotidien. Dans le nouveau code, vous pouvez utiliser des spécifications basées sur SAL par conception tout au long de ; dans le code plus ancien, vous pouvez ajouter des annotations de manière incrémentielle et ainsi augmenter les avantages chaque fois que vous mettez à jour.
Les en-têtes publics Microsoft sont déjà annotés. Par conséquent, nous vous suggérons que dans vos projets, vous annotez d’abord les fonctions de nœud feuille et les fonctions qui appellent des API Win32 pour tirer le meilleur parti.
Quand puis-je annoter ?
Voici quelques recommandations :
Annotez tous les paramètres de pointeur.
Annotez les annotations de plage de valeurs afin que l’analyse du code puisse garantir la sécurité de la mémoire tampon et du pointeur.
Annoter les règles de verrouillage et les effets secondaires de verrouillage. Pour plus d’informations, consultez Annoter le comportement de verrouillage.
Annoter les propriétés du pilote et d’autres propriétés spécifiques au domaine.
Vous pouvez également annoter tous les paramètres pour rendre votre intention claire tout au long et pour faciliter la vérification de l’exécution des annotations.
Voir aussi
- Utilisation d’annotations SAL pour réduire les défauts du code C/C++
- Annotation des paramètres de fonction et des valeurs de retour
- Annotation du comportement d’une fonction
- Annotations des structs et des classes
- Annotation du comportement de verrouillage
- Spécification du moment et de l’endroit où une annotation s’applique
- Bonnes pratiques et exemples