Avviso C26837
Il valore per il comparand
comp
per la funzionefunc
è stato caricato dalla posizionedest
di destinazione tramite la lettura non volatile.
Questa regola è stata aggiunta in Visual Studio 2022 17.8.
Osservazioni:
La InterlockedCompareExchange
funzione e i relativi derivati, ad InterlockedCompareExchangePointer
esempio , eseguono un'operazione atomica di confronto e scambio sui valori specificati. Se il Destination
valore è uguale al Comparand
valore , il valore di scambio viene archiviato nell'indirizzo specificato da Destination
. In caso contrario, non viene eseguita alcuna operazione. Le interlocked
funzioni forniscono un meccanismo semplice per sincronizzare l'accesso a una variabile condivisa da più thread. Questa funzione è atomica rispetto alle chiamate ad altre interlocked
funzioni. L'uso improprio di queste funzioni può generare codice oggetto che si comporta in modo diverso da quello previsto perché l'ottimizzazione può modificare il comportamento del codice in modi imprevisti.
Si consideri il seguente codice :
#include <Windows.h>
bool TryLock(__int64* plock)
{
__int64 lock = *plock;
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
Lo scopo di questo codice è:
- Leggere il valore corrente dal
plock
puntatore. - Controllare se questo valore corrente ha il bit meno significativo impostato.
- Se ha un bit meno significativo impostato, cancellare il bit mantenendo gli altri bit del valore corrente.
A tale scopo, una copia del valore corrente viene letta dal plock
puntatore e salvata in una variabile lock
dello stack . lock
viene usato tre volte:
- Prima di tutto, per verificare se è impostato il bit meno significativo.
- In secondo luogo, come
Comparand
valore diInterlockedCompareExchange64
. - Infine, nel confronto del valore restituito da
InterlockedCompareExchange64
Ciò presuppone che il valore corrente salvato nella variabile dello stack venga letto una sola volta all'inizio della funzione e non cambi. Ciò è necessario perché il valore corrente viene prima controllato prima di tentare l'operazione, quindi usato in modo esplicito come Comparand
in InterlockedCompareExchange64
e infine usato per confrontare il valore restituito da InterlockedCompareExchange64
.
Sfortunatamente, il codice precedente può essere compilato in assembly che si comporta in modo diverso rispetto a quello previsto dal codice sorgente. Compilare il codice precedente con il compilatore Microsoft Visual C++ (MSVC) e l'opzione /O1
ed esaminare il codice assembly risultante per vedere come viene ottenuto il valore del blocco per ognuno dei riferimenti a lock
. La versione del compilatore MSVC v19.37 produce codice assembly simile al seguente:
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
contiene il valore del parametro plock
. Invece di creare una copia del valore corrente nello stack, il codice dell'assembly legge nuovamente il valore da plock
ogni volta. Ciò significa che il valore può essere diverso ogni volta che viene letto. Ciò invalida la purificazione eseguita dallo sviluppatore. Il valore viene rilette dopo che è stato verificato che ha il relativo bit meno significativo impostato.The value is re-read from plock
after it's verified that it has its least-significant bit set. Poiché viene rilette dopo l'esecuzione di questa convalida, il nuovo valore potrebbe non avere più il bit meno significativo impostato. In una race condition questo codice potrebbe comportarsi come se fosse stato ottenuto correttamente il blocco specificato quando era già bloccato da un altro thread.
Il compilatore può rimuovere o aggiungere letture o scritture di memoria, purché il comportamento del codice non venga modificato. Per impedire al compilatore di apportare tali modifiche, forzare le letture quando volatile
si legge il valore dalla memoria e memorizzarlo nella cache in una variabile. Gli oggetti dichiarati come volatile
non vengono usati in determinate ottimizzazioni perché i relativi valori possono cambiare in qualsiasi momento. Il codice generato legge sempre il valore corrente di un volatile
oggetto quando viene richiesto, anche se un'istruzione precedente richiede un valore dello stesso oggetto. Il contrario si applica anche per lo stesso motivo. Il valore dell'oggetto volatile
non viene letto di nuovo a meno che non venga richiesto. Per altre informazioni su volatile
, vedere volatile
. Ad esempio:
#include <Windows.h>
bool TryLock(__int64* plock)
{
__int64 lock = *static_cast<volatile __int64*>(plock);
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
Compilare questo codice con la stessa /O1
opzione di prima. L'assembly generato non legge plock
più per l'uso del valore memorizzato nella cache in lock
.
Per altri esempi di come è possibile correggere il codice, vedere Esempio.
Nome dell'analisi del codice: INTERLOCKED_COMPARE_EXCHANGE_MISUSE
Esempio
Il compilatore potrebbe ottimizzare il codice seguente per leggere plock
più volte anziché usare il valore memorizzato nella cache in lock
:
#include <Windows.h>
bool TryLock(__int64* plock)
{
__int64 lock = *plock;
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
Per risolvere il problema, forzare le letture in volatile
modo che il compilatore non ottimizzi il codice per la lettura successivamente dalla stessa memoria, a meno che non venga esplicitamente richiesto. Ciò impedisce all'ottimizzatore di introdurre un comportamento imprevisto.
Il primo metodo per considerare la memoria come volatile
deve accettare l'indirizzo di destinazione come volatile
puntatore:
#include <Windows.h>
bool TryLock(volatile __int64* plock)
{
__int64 lock = *plock;
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
Il secondo metodo usa volatile
la lettura dall'indirizzo di destinazione. Esistono diversi modi per eseguire questa operazione:
- Cast del puntatore al
volatile
puntatore prima di dereferenziare il puntatore - Creazione di un
volatile
puntatore dal puntatore fornito - Uso delle
volatile
funzioni helper di lettura.
Ad esempio:
#include <Windows.h>
bool TryLock(__int64* plock)
{
__int64 lock = ReadNoFence64(plock);
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
Euristica
Questa regola viene applicata rilevando se il valore nella Destination
funzione InterlockedCompareExchange
o uno dei suoi derivati viene caricato tramite un valore nonvolatile
letto e quindi usato come Comparand
valore. Tuttavia, non controlla in modo esplicito se il valore caricato viene usato per determinare il valore di scambio . Presuppone che il valore di scambio sia correlato al Comparand
valore.
Vedi anche
InterlockedCompareExchange
function (winnt.h)
_InterlockedCompareExchange
funzioni intrinseche