Avertissement C26837
La valeur du comparand
comp
pour la fonctionfunc
a été chargée à partir de l’emplacement de destinationdest
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 :
- Lisez la valeur actuelle à partir du pointeur
plock
. - Vérifiez si cette valeur actuelle a le bit le moins significatif défini.
- 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 :
- Tout d’abord, pour vérifier si le bit le moins significatif est défini.
- Deuxièmement, comme
Comparand
valeur àInterlockedCompareExchange64
. - 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