Présentation de SAL
Le langage d'annotation du code source Microsoft (SAL) fournit un ensemble d'annotations que vous pouvez utiliser pour décrire comment une fonction utilise ses paramètres, les hypothèses qu'il effectue sur eux, et les garanties qu'il fait lorsqu´elle se termine.Les annotations sont définies dans le fichier d'en-tête <sal.h>.Visual Studio code analysis for C++ utilise des annotations SAL pour modifier son analyse des fonctions.Pour plus d'informations sur le SAL 2.0 pour le développement de pilotes windows, consultez Annotations SAL 2,0 pour les pilotes windows.
En mode natif, C et C++ contiennent uniquement des manières limitées pour que les développeurs expriment de manière consistante les intentions et l´invariance.À l'aide des annotations SAL, vous pouvez décrire les fonctions plus en détail afin que les développeurs qui les utilisent puissent mieux comprendre comment les utiliser.
Qu'est-ce que SAL et pourquoi vous devez l'utiliser ?
Simplement indiqué, le SAL est une méthode économique qui permet de laisser le compilateur tester votre code pour vous.
SAL rend le code plus importants
Le SAL peut vous aider à rendre votre conception du code plus compréhensible, pour les humains et les outils d'analyse du code.Considérez cet exemple qui montre la fonction C d'exécution memcpy:
void * memcpy(
void *dest,
const void *src,
size_t count
);
Pouvez -vous indiquer ce que cette fonction fait?Lorsqu'une fonction est implémentée ou appelée, certaines propriétés doivent être mises à jour pour vérifier l'exactitude du programme.Juste en recherchant une déclaration telle que celle de l'exemple, vous ne savez pas ce qu´elles sont.Sans annotations SAL, vous devez avoir recours à la documentation ou aux commentaires de code.Voici ce que la documentation MSDN indique pour memcpy:
"Copie le nombre de bytes de src à dest.Le comportement de mempcy n'est pas spécifié si les zones source et de destination se superposent.Memmove permet de gérer des régions qui se chevauchent.Remarque sur la sécurité : Assurez -vous que la mémoire tampon de destination est 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 un couple de bits d´informations qui laissent entendre que votre code doit mettre à jour certaines propriétés pour vérifier l'exactitude de programme :
memcpy copie le count des octets de la mémoire tampon source à 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 ou 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 efficacement deviner également une relation.Le SAL peut fournir plus de clarté sur les propriétés et l'implémentation de la fonction, comme indiqué ci-après :
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 dans la documentation MSDN, mais elles sont plus concises et elles 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 des problèmes de sécurité de dépassement de mémoire tampon.Encore mieux, les modèles de sémantique que le SAL fournit peut améliorer l'efficacité des outils d'analyse du code automatisés dans la découverte des bogues potentiels.Imaginez que quelqu'un écrit cette implémentation avec erreurs 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 commune off-by-one.Heureusement, l'auteur du code a fourni l'annotation de la taille de la m'emoire tampon de l'outil d'analyse du code - un outil d'analyse de code peut intercepter le bogue en analysant la seule fonction.
Éléments fondamentaux relatifs à SAL
SAL définit quatre types de paramètres de base, qui sont catégorisés par le modèle d'utilisation.
Catégorie |
Annotation de paramètre |
Description |
---|---|---|
Entrée de la fonction appelée |
_In_ |
Les données sont passées à la fonction appelée, et sont traitées en lecture seule. |
Entrée de la fonction appelée, et à la sortie à l'appelant |
_Inout_ |
Les données utilisables sont passées à la fonction et potentiellement sont modifiées. |
Sortie à l'appelant |
_Out_ |
L'appelant fournit uniquement de l'espace pour l´écriture à la fonction appelée.La fonction appelée écrit des données dans cet espace. |
Sortie de pointeur vers l'appelant |
_Outptr_ |
Comme Output to caller.La valeur retournée par la fonction appelée est un pointeur. |
Ces quatre annotations de base peuvent être rendues plus explicites de plusieurs façons.Par défaut, les paramètres du pointeur annoté sont supposés indispensables- ils doivent être non-NULL pour que la fonction réussisse.La variation la plus fréquemment utilisée des annotations de base indique qu'un paramètre de pointeur est facultatif- s'il est NULL, la fonction peut encore réussir à effectuer son travail.
Ce tableau montre comment distinguer entre les paramètres requis et les paramètres optionnels :
Les paramètres sont requis. |
Paramètres facultatifs |
|
---|---|---|
Entrée de la fonction appelée |
_In_ |
_In_opt_ |
Entrée de la fonction appelée, et à la sortie à l'appelant |
_Inout_ |
_Inout_opt_ |
Sortie à l'appelant |
_Out_ |
_Out_opt_ |
Sortie de pointeur vers l'appelant |
_Outptr_ |
_Outptr_opt_ |
L'utilisation de ces annotations identifient des valeurs non initialisées possibles et des utilisations non valides de pointeur null de manière formelle et exacte.Passer NULL à un paramètre obligatoire peut provoquer un blocage, ou il peut provoquer un code d´erreur « échec» à retourner.L'un ou l'autre, la fonction ne peut pas réussir en réalisant son travail.
Exemples SAL
Cette section présente des exemples de code pour les annotations SAL de base.
Utilisation de l'outil d'analyse du code Visual Studio pour rechercher des erreurs
Dans les exemples, l'outil d'analyse du code Visual Studio est utilisé avec des annotations SAL pour rechercher des erreurs de code.Voici comment procéder.
Pour utiliser les outils d'analyse du code Visual Studio et le SAL
Dans Visual Studio, ouvrez projet c++ qui contient des annotations SAL.
Dans le menu choisissez GénérerExécuter l'analyse du code sur la solution.
Prenons l'exemple de _In_ de cette section.Si vous exécutez l'analyse du code sur celui-ci, cet avertissement s'affiche :
C6387 Valeur de paramètre non valide´pInt´ peut être '0' : ceci n'est pas conforme à la spécification de la fonction 'InCallee'.
Exemple : L'annotation _In_
L'annotation _In_ indique que :
Le paramètre doit être valide et ne sera pas modifié.
La fonction lira uniquement de la mémoire tampon à un seul élément.
L'appelant doit fournir la mémoire tampon et l'initialiser.
_In_ Spécifie « lecture seule ».Une erreur courante consiste à appliquer _In_ à un paramètre qui doit avoir l'annotation _Inout_ à la place.
_In_ est autorisé mais ignorée par l'analyseur sur les scalaires non pointeur.
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 l'analyse du code Visual Studio dans cet exemple, il vérifie que les appelants passent un pointeur non null à une mémoire tampon initialisée pour pInt.Dans ce cas le pointeur pInt ne peut pas avoir la valeur NULL.
Exemple : L'annotation de _In_opt_
_In_opt_ est identique à _In_, mais le paramètre d'entrée est autorisé à être NULL et, par conséquent, la fonction doit vérifier ca.
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);
}
L'analyse du code Visual Studio valide que la fonction vérifie les NULL avant qu'elle accède à la mémoire tampon.
Exemple : L'annotation de _Out_
_Out_ prend en charge un scénario courant dans lequel un pointeur non null qui pointe vers une mémoire tampon d'élément est passé et la fonction initialise l'élément.L'appelant ne doit pas initialiser la mémoire tampon avant l'appel ; la fonction appelée promet l'initialisation avant de la 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;
}
L'outil d'analyse du code Visual Studio valide que l'appelant passe un pointeur non null à une mémoire tampon pour pInt et que la mémoire tampon est initialisée par la fonction avant de retourner.
Exemple : L'annotation de _Out_opt_
_Out_opt_ est identique à _Out_, mais le paramètre d'entrée est autorisé à être NULL et, par conséquent, la fonction doit vérifier ca.
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);
}
L'analyse du code Visual Studio vérifie que la fonctione vérifie les NULL avant que pInt soit déréférencé, et si pInt n'est pas nul, que la mémoire tampon est initialisé par la fonction avant de retourner.
Exemple : L'annotation de _Inout_
_Inout_ est utilisée 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 est modifié, il doit toujours avoir une valeur valide au retour.L'annotation spécifie que la fonction peut librement lire et écrire 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
}
L'analyse du code Visual Studio vérifie que les appelants passent un pointeur non null à une mémoire tampon initialisée pour pInt, et que, avant retour, pInt est toujours non null et la mémoire tampon est initialisée.
Exemple : L'annotation de _Inout_opt_
_Inout_opt_ est identique à _Inout_, mais le paramètre d'entrée est autorisé à être NULL et, par conséquent, la fonction doit vérifier ca.
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);
}
L'analyse du code Visual Studio valide que la fonctione vérifie les NULL avant qu´il accède au buffer et si pInt n´est pas NULL, que la mémoire tampon est initialisé par la fonction avant de retourner.
Exemple : L'annotation de _Outptr_
_Outptr_ est utilisé pour annoter un paramètre qui est conçu pour retourner un pointeur.Le paramètre ne doit pas être NULL, et la fonction appelé retour un pointeur non-NULL dedans et que le 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);
}
L'outil d'analyse du code Visual Studio 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 : L'annotation de _Outptr_opt_
_Outptr_opt_ est identique à _Outptr_, mais 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);
}
L'analyse du code Visual Studio vérifie que la fonctione vérifie les NULL avant que *pInt soit déréférencé, et que le buffer est initialisé par la fonction avant de retourner.
Exemple : L'annotation _Success_ en association avec le _Out_
Les annotations peuvent être appliquées à la plupart des objets.En particulier, vous pouvez annoter une fonction entière.L'une des spécifications 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 le succès ou l'échec de la fonction.À l'aide de l'annotation _Success_ , vous pouvez indiquer à quoi le succès pour une fonction ressemble.Le paramètre à l'annotation _Success_ est juste une expression qui lorsqu'il a la valeur true indique que la fonction a réussi.L'expression peut être n´importe quoi que l'analyseur d'annotation peut gérer.Les effets des annotations après le retour de la fonction s'appliquent uniquement lorsque la fonction réussit.Cet exemple montre comment _Success_ interagit 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 de _Out_ fait valider l'analyse du code Visual Studio que l'appelant passe un pointeur non null à une mémoire tampon pour pInt, et que la mémoire tampon est initialisée par la fonction avant de retourner.
Bonne pratique SAL
Ajouter des annotations au code existant
SAL est une technologie performante qui peut vous aider à améliorer la fiabilité et la sécurité 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 caractéristiques basées sur SAL par conception partout ; dans le code plus ancien, vous pouvez ajouter des annotations de façon incrémentielle et augmenter ainsi les avantages chaque fois que vous mettez à jour.
Les en-têtes publics de Microsoft sont annotés déjà.Par conséquent, nous vous suggérons dans vos projets que vous annotiez d'abord les fonctions de nœud terminal et les fonctions qui appellent des API Win32 pour obtenir la plupart des avantages.
Quand dois-je annoter ?
Voici quelques indications :
Annoter 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 mémoire tampon et de pointeur.
Annoter des règles de verrouillage et les effets secondaires des verrouillages.Pour plus d'informations, consultez Annotation du comportement de verrouillage.
Annotez les propriétés de gestionnaire et d'autres propriétés spécifiques au domaine.
Ou vous pouvez annoter tous les paramètres pour rendre votre intention claire dans l'ensemble et pour la rendre facile de vérifier que les annotations ont été effectuées.
Ressources connexes
Le Blog de l'Équipe d'Analyse du Code
Voir aussi
Référence
Annotation de paramètres de fonction et valeurs de retour
Annotation du comportement d'une fonction
Structs et classes d'annotation
Annotation du comportement de verrouillage
Spécification du moment où une annotation est applicable et dans quel cas
Meilleures pratiques et exemples (SAL)
Autres ressources
Utilisation d'annotations SAL pour réduire les défauts du code C/C++