Warnung C26837

Der Wert für den Vergleichswert comp für die Funktion func wurde vom Zielspeicherort dest über nicht veränderliche Lesevorgänge geladen.

Diese Regel wurde in Visual Studio 2022 17.8 hinzugefügt.

Hinweise

Die InterlockedCompareExchange-Funktion und ihre Ableitungen wie InterlockedCompareExchangePointer führen einen atomischen Vergleichs- und Austauschvorgang für die angegebenen Werte durch. Wenn der Destination-Wert gleich dem Comparand-Wert ist, wird der exchange-Wert in der von Destination angegebenen Adresse gespeichert. Andernfalls wird kein Vorgang ausgeführt. Die interlocked-Funktionen bieten einen einfachen Mechanismus zum Synchronisieren des Zugriffs auf eine Variable, die von mehreren Threads gemeinsam genutzt wird. Diese Funktion ist atomisch und berücksichtigt Aufrufe anderer interlocked-Funktionen. Der falsche Gebrauch dieser Funktionen kann Objektcode generieren, der sich ungewöhnlich verhält, da die Optimierung das Verhalten des Codes auf unerwartete Weise ändern kann.

Betrachten Sie folgenden Code:

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

Der Zweck dieses Codes ist:

  1. den aktuellen Wert aus dem Zeiger plock lesen
  2. überprüfen, ob bei diesem aktuellen Wert das am wenigsten signifikante Bit festgelegt ist
  3. wenn das am wenigsten signifikante Bit festgelegt ist, das Bit löschen, während die anderen Bits des aktuellen Werts beibehalten werden

Hierzu wird eine Kopie des aktuellen Werts aus dem Zeiger plock gelesen und in einer Stapelvariablen lock gespeichert. lock wird dreimal verwendet:

  1. erstens zum Überprüfen, ob das am wenigsten signifikante Bit festgelegt ist
  2. zweitens als Comparand-Wert für InterlockedCompareExchange64
  3. zuletzt im Vergleich des Rückgabewerts von InterlockedCompareExchange64

Dabei wird davon ausgegangen, dass der aktuelle Wert, der in der Stapelvariable gespeichert ist, einmal am Anfang der Funktion gelesen wird und sich nicht ändert. Das ist erforderlich, da der aktuelle Wert zuerst überprüft wird, bevor der Vorgang versucht wird, dann explizit als Comparand in InterlockedCompareExchange64 verwendet und schließlich verwendet wird, um den Rückgabewert von InterlockedCompareExchange64 zu vergleichen.

Leider kann der vorherige Code in eine Assembly kompiliert werden, die sich anders verhält als das, was Sie vom Quellcode erwarten. Kompilieren Sie den vorherigen Code mit dem Microsoft Visual C++-Compiler (MSVC) und der Option /O1, und überprüfen Sie den resultierenden Assemblycode, um zu sehen, wie der Wert der Sperre für jeden der Verweise auf lock abgerufen wird. Die MSVC-Compilerversion 19.37 erzeugt Assemblycode, der wie folgt aussieht:

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 enthält den Wert des Parameters plock. Anstatt eine Kopie des aktuellen Werts im Stapel zu erstellen, liest der Assemblycode den Wert jedes Mal neu aus plock. Dies bedeutet, dass der Wert jedes Mal anders sein könnte, wenn er gelesen wird. Dadurch wird die vom Entwickler durchgeführte Bereinigung ungültig. Der Wert wird aus plock neu gelesen, nachdem überprüft wurde, ob das am wenigsten signifikante Bit festgelegt ist. Da er nach der Überprüfung erneut gelesen wird, weist der neue Wert möglicherweise nicht mehr das am wenigsten signifikante Bit auf. Unter einer Racebedingung verhält sich dieser Code möglicherweise so, als ob die angegebene Sperre erfolgreich angewendet wurde, als er bereits durch einen anderen Thread gesperrt wurde.

Der Compiler darf Lese- oder Schreibvorgänge im Arbeitsspeicher entfernen oder hinzufügen, solange das Verhalten des Codes nicht geändert wird. Um zu verhindern, dass der Compiler solche Änderungen vornimmt, wird erzwungen, dass Lesevorgänge volatile werden, wenn Sie den Wert aus dem Arbeitsspeicher lesen und in einer Variablen zwischenspeichern. Objekte, die als volatile deklariert werden, werden in bestimmten Optimierungen nicht verwendet, da sich ihre Werte jederzeit ändern können. Der generierte Code liest bei jeder Anforderung den aktuellen Wert eines volatile-Objekts, selbst wenn eine vorherige Anweisung bereits den Wert des gleichen Objekts abgefragt hatte. Die Umgekehrte gilt aus demselben Grund ebenfalls. Der Wert des volatile-Objekts wird nicht erneut gelesen, es sei denn, es wird angefordert. Weitere Informationen zu volatile finden Sie unter volatile. Zum Beispiel:

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

Kompilieren Sie diesen Code mit derselben /O1-Option wie zuvor. Die generierte Assembly liest nicht mehr plock zur Verwendung des zwischengespeicherten Werts in lock.

Weitere Beispiele dazu, wie der Code korrigiert werden kann, finden Sie unter Beispiel.

Name der Codeanalyse: INTERLOCKED_COMPARE_EXCHANGE_MISUSE

Beispiel

Der Compiler optimiert möglicherweise den folgenden Code, um plock mehrmals zu lesen, anstatt den zwischengespeicherten Wert in lock zu verwenden:

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

Um das Problem zu beheben, erzwingen Sie, dass Lesevorgänge volatile sind, damit der Compiler Code nicht so optimiert, dass er nacheinander aus demselben Arbeitsspeicher gelesen wird, es sei denn, er wird explizit angewiesen. Dadurch wird verhindert, dass der Optimierer unerwartetes Verhalten einführt.

Die erste Methode zum Behandeln des Arbeitsspeichers als volatile besteht darin, die Zieladresse als volatile-Zeiger zu übernehmen:

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

Die zweite Methode verwendet volatile zum Lesen aus der Zieladresse. Hierfür gibt es verschiedene Möglichkeiten:

  • Umwandeln des Zeigers in einen volatile-Zeiger vor der Dereferenzierung des Zeigers
  • Erstellen eines volatile-Zeigers aus dem angegebenen Zeiger
  • Verwenden von volatile-Hilfsfunktionen zum Lesen

Zum Beispiel:

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

Heuristik

Diese Regel wird erzwungen, indem ermittelt wird, ob der Wert in Destination der InterlockedCompareExchange-Funktion oder einer seiner Ableitungen über ein Nicht-volatile-Lesevorgang geladen und dann als Comparand-Wert verwendet wird. Es wird jedoch nicht explizit überprüft, ob der geladene Wert verwendet wird, um den exchange-Wert zu ermitteln. Es wird davon ausgegangen, dass der exchange-Wert mit dem Wert Comparand in Verbindung steht.

Siehe auch

InterlockedCompareExchange-Funktion (winnt.h)
Intrinsische _InterlockedCompareExchange-Funktionen