Condividi tramite


Avviso C26837

Il valore per il comparand comp per la funzione func è stato caricato dalla posizione dest 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 InterlockedCompareExchangePointeresempio , 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 è:

  1. Leggere il valore corrente dal plock puntatore.
  2. Controllare se questo valore corrente ha il bit meno significativo impostato.
  3. 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 lockdello stack . lock viene usato tre volte:

  1. Prima di tutto, per verificare se è impostato il bit meno significativo.
  2. In secondo luogo, come Comparand valore di InterlockedCompareExchange64.
  3. 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 InterlockedCompareExchange64e 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