Partager via


Avertissement C26837

La valeur du comparand comp pour la fonction func a été chargée à partir de l’emplacement de destination dest via une lecture non volatile.

Cette règle a été ajoutée dans Visual Studio 2022 17.8.

Notes

La fonction InterlockedCompareExchange et ses dérivés tels que InterlockedCompareExchangePointer, effectuent une opération atomique de comparaison et d’échange sur les valeurs spécifiées. Si la valeur Destination est égale à la valeur Comparand, la valeur Exchange est stockée dans l'adresse spécifiée par Destination. Dans le cas contraire, aucune opération n'est effectuée. Les fonctions interlocked fournissent un mécanisme simple pour synchroniser l’accès à une variable partagée par plusieurs threads. Cette fonction est atomique par rapport aux appels à d’autres fonctions interlocked. Une mauvaise utilisation de ces fonctions peut générer du code objet qui se comporte différemment de celui attendu, car l’optimisation peut modifier le comportement du code de manière inattendue.

Prenez le code suivant :

#include <Windows.h> 
 
bool TryLock(__int64* plock) 
{ 
    __int64 lock = *plock; 
    return (lock & 1) && 
        _InterlockedCompareExchange64(plock, lock & ~1, lock) == lock; 
} 

L’intention de ce code est la suivante :

  1. Lisez la valeur actuelle à partir du pointeur plock.
  2. Vérifiez si cette valeur actuelle a le bit le moins significatif défini.
  3. S’il a le bit le moins significatif défini, effacez le bit tout en conservant les autres bits de la valeur actuelle.

Pour ce faire, une copie de la valeur actuelle est lue à partir du pointeur plock et enregistrée dans une variable de pile lock. lock est utilisé trois fois :

  1. Tout d’abord, pour vérifier si le bit le moins significatif est défini.
  2. Deuxièmement, comme Comparand valeur à InterlockedCompareExchange64.
  3. Enfin, dans la comparaison de la valeur de retour de InterlockedCompareExchange64

Cela suppose que la valeur actuelle enregistrée dans la variable de pile est lue une fois au début de la fonction et ne change pas. Cela est nécessaire, car la valeur actuelle est d’abord vérifiée avant d’essayer l’opération, puis explicitement utilisée comme Comparand dans InterlockedCompareExchange64, et enfin utilisée pour comparer la valeur de retour à partir de InterlockedCompareExchange64.

Malheureusement, le code précédent peut être compilé dans un assembly qui se comporte différemment de ce que vous attendez du code source. Compilez le code précédent avec le compilateur Microsoft Visual C++ (MSVC) et l’option /O1 et inspectez le code d’assembly résultant pour voir comment la valeur du verrou pour chacune des références à obtenir lock . La version du compilateur MSVC v19.37 produit le code d’assembly qui ressemble à ceci :

plock$ = 8 
bool TryLock(__int64 *) PROC                          ; TryLock, COMDAT 
        mov     r8b, 1 
        test    BYTE PTR [rcx], r8b 
        je      SHORT $LN3@TryLock 
        mov     rdx, QWORD PTR [rcx] 
        mov     rax, QWORD PTR [rcx] 
        and     rdx, -2 
        lock cmpxchg QWORD PTR [rcx], rdx 
        je      SHORT $LN4@TryLock 
$LN3@TryLock: 
        xor     r8b, r8b 
$LN4@TryLock: 
        mov     al, r8b 
        ret     0 
bool TryLock(__int64 *) ENDP                          ; TryLock 

rcx contient la valeur du paramètre plock. Au lieu d’effectuer une copie de la valeur actuelle sur la pile, le code d’assembly lit à nouveau la valeur à partir de plock à chaque fois. Cela signifie que la valeur peut être différente chaque fois qu’elle est lue. Cela invalide l’assainissement que le développeur effectue. La valeur est réécrite plock après avoir vérifié qu’elle a son jeu de bits le moins significatif. Étant donné qu’elle est réécrite une fois cette validation effectuée, la nouvelle valeur peut ne plus avoir le jeu de bits le moins significatif. Dans une condition de concurrence, ce code peut se comporter comme s’il a obtenu le verrou spécifié lorsqu’il a déjà été verrouillé par un autre thread.

Le compilateur est autorisé à supprimer ou à ajouter des lectures ou des écritures de mémoire tant que le comportement du code n’est pas modifié. Pour empêcher le compilateur d’apporter de telles modifications, forcez les lectures à être volatile lorsque vous lisez la valeur de la mémoire et mettez-la en cache dans une variable. Les objets déclarés comme volatile ne sont pas utilisés dans certaines optimisations, car leurs valeurs peuvent changer à tout moment. Le code généré lit toujours la valeur actuelle d'un objet volatile lorsque la demande en est faite, même si une instruction précédente a demandé une valeur du même objet. L’inverse s’applique également pour la même raison. La valeur de l’objet volatile n’est pas lue à nouveau, sauf si elle est demandée. Pour plus d'informations sur volatile, consultez volatile. Par exemple :

#include <Windows.h> 
 
bool TryLock(__int64* plock) 
{ 
    __int64 lock = *static_cast<volatile __int64*>(plock); 
    return (lock & 1) && 
        _InterlockedCompareExchange64(plock, lock & ~1, lock) == lock; 
}

Compilez ce code avec la même option /O1 que précédemment. L’assembly généré ne lit plus plock pour l’utilisation de la valeur mise en cache dans lock.

Pour obtenir d’autres exemples de correction du code, consultez l’exemple.

Nom de l’analyse du code : INTERLOCKED_COMPARE_EXCHANGE_MISUSE

Exemple

Le compilateur peut optimiser le code suivant pour lire plock plusieurs fois au lieu d’utiliser la valeur mise en cache dans lock :

#include <Windows.h> 
 
bool TryLock(__int64* plock) 
{ 
    __int64 lock = *plock; 
    return (lock & 1) && 
        _InterlockedCompareExchange64(plock, lock & ~1, lock) == lock; 
}

Pour résoudre le problème, forcez les lectures à être volatile afin que le compilateur n’optimise pas le code pour lire successivement à partir de la même mémoire, sauf indication explicite. Cela empêche l’optimiseur d’introduire un comportement inattendu.

La première méthode pour traiter la mémoire comme volatile consiste à prendre l’adresse de destination comme pointeur volatile :

#include <Windows.h> 
 
bool TryLock(volatile __int64* plock) 
{ 
    __int64 lock = *plock; 
    return (lock & 1) && 
        _InterlockedCompareExchange64(plock, lock & ~1, lock) == lock; 
} 

La deuxième méthode utilise la lecture volatile à partir de l’adresse de destination. Voici les différentes manières de le faire :

  • Conversion du pointeur en pointeur volatile avant de déreferencer le pointeur
  • Création d’un pointeur volatile à partir du pointeur fourni
  • Utilisation des fonctions d’assistance de lecture volatile.

Par exemple :

#include <Windows.h> 
 
bool TryLock(__int64* plock) 
{ 
    __int64 lock = ReadNoFence64(plock); 
    return (lock & 1) && 
        _InterlockedCompareExchange64(plock, lock & ~1, lock) == lock; 
}

Heuristique

Cette règle est appliquée en détectant si la valeur dans Destination de la fonction InterlockedCompareExchange, ou l’un de ses dérivés, est chargée via une lecture non-volatile lue, puis utilisée comme valeur Comparand. Toutefois, elle ne vérifie pas explicitement si la valeur chargée est utilisée pour déterminer la valeur d’échange. Elle suppose que la valeur d’échange est liée à la valeur Comparand.

Voir aussi

Fonction (winnt.h) InterlockedCompareExchange
Fonctions intrinsèques _InterlockedCompareExchange