Warnung C26837
Der Wert für den Vergleichswert
comp
für die Funktionfunc
wurde vom Zielspeicherortdest
ü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:
- den aktuellen Wert aus dem Zeiger
plock
lesen - überprüfen, ob bei diesem aktuellen Wert das am wenigsten signifikante Bit festgelegt ist
- 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:
- erstens zum Überprüfen, ob das am wenigsten signifikante Bit festgelegt ist
- zweitens als
Comparand
-Wert fürInterlockedCompareExchange64
- 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