Upozornění C26837
Hodnota pro porovnávací funkci
comp
func
byla načtena z cílového umístěnídest
prostřednictvím nestálého čtení.
Toto pravidlo bylo přidáno v sadě Visual Studio 2022 17.8.
Poznámky
Funkce InterlockedCompareExchange
a její deriváty, jako InterlockedCompareExchangePointer
je například , proveďte atomovou operaci porovnání a výměny se zadanými hodnotami. Destination
Je-li hodnota rovna hodnotěComparand
, hodnota výměny je uložena v adrese určené Destination
. V opačném případě se neprovádí žádná operace. Funkce interlocked
poskytují jednoduchý mechanismus pro synchronizaci přístupu k proměnné, která je sdílena více vlákny. Tato funkce je atomická s ohledem na volání jiných interlocked
funkcí. Zneužití těchto funkcí může generovat kód objektu, který se chová jinak, než očekáváte, protože optimalizace může změnit chování kódu neočekávanými způsoby.
Uvažujte následující kód:
#include <Windows.h>
bool TryLock(__int64* plock)
{
__int64 lock = *plock;
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
Záměrem tohoto kódu je:
- Přečtěte si aktuální hodnotu z
plock
ukazatele. - Zkontrolujte, jestli má tato aktuální hodnota nastavenou nejméně významnou bitovou sadu.
- Pokud má nejméně významnou sadu bitů, vymažte bit při zachování ostatních bitů aktuální hodnoty.
K tomu se z ukazatele načte plock
kopie aktuální hodnoty a uloží se do proměnné lock
zásobníku . lock
se používá třikrát:
- Nejprve zkontrolujte, jestli je nastavený nejméně významný bit.
- Za druhé, jako
Comparand
hodnota doInterlockedCompareExchange64
. - Nakonec ve srovnání s vrácenou hodnotou z
InterlockedCompareExchange64
Předpokládá se, že aktuální hodnota uložená do proměnné zásobníku se na začátku funkce načte a nezmění se. To je nezbytné, protože aktuální hodnota je nejprve zkontrolována před pokusem o operaci, pak explicitně použita jako Comparand
in InterlockedCompareExchange64
a nakonec použita k porovnání návratové hodnoty z InterlockedCompareExchange64
.
Předchozí kód lze bohužel zkompilovat do sestavení, které se chová jinak než od toho, co očekáváte od zdrojového kódu. Zkompilujte předchozí kód pomocí kompilátoru Microsoft Visual C++ (MSVC) a /O1
zkontrolujte výsledný kód sestavení, abyste zjistili, jak se získá hodnota zámku pro každý odkaz na získání lock
. Kompilátor MSVC verze v19.37 vytvoří kód sestavení, který vypadá takto:
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
obsahuje hodnotu parametru plock
. Místo vytvoření kopie aktuální hodnoty v zásobníku kód sestavení znovu čte hodnotu pokaždé plock
. To znamená, že hodnota se může při každém čtení lišit. Tím se zneplatní sanitizace, kterou vývojář provádí. Hodnota se znovu načte po plock
ověření, že má jeho nejméně významnou bitovou sadu. Vzhledem k tomu, že se po provedení tohoto ověření znovu přečte, nová hodnota už nemusí obsahovat nejméně významnou bitovou sadu. Pod podmínkou časování se tento kód může chovat, jako by úspěšně získal zadaný zámek, když byl již uzamčen jiným vláknem.
Kompilátor může odebrat nebo přidat čtení paměti nebo zápisy, pokud chování kódu není změněno. Chcete-li zabránit kompilátoru v provádění těchto změn, vynuťte čtení, aby volatile
byla při čtení hodnoty z paměti a uložena v mezipaměti v proměnné. Objekty deklarované jako volatile
se v určitých optimalizacích nepoužívají, protože jejich hodnoty se můžou kdykoli změnit. Vygenerovaný kód vždy čte aktuální hodnotu objektu volatile
, když je požadován, i když předchozí instrukce požádala o hodnotu ze stejného objektu. Opak platí také pro stejný důvod. Hodnota objektu volatile
se znovu nečte, pokud není požadována. Další informace o produktu naleznete v volatile
tématu volatile
. Příklad:
#include <Windows.h>
bool TryLock(__int64* plock)
{
__int64 lock = *static_cast<volatile __int64*>(plock);
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
Zkompilujte tento kód se stejnou /O1
možností jako předtím. Vygenerované sestavení již nečte plock
pro použití hodnoty uložené v mezipaměti v lock
.
Další příklady pevného kódu najdete v příkladu.
Název analýzy kódu: INTERLOCKED_COMPARE_EXCHANGE_MISUSE
Příklad
Kompilátor může optimalizovat následující kód tak, aby četl plock
vícekrát, místo aby používal hodnotu uloženou v mezipaměti v lock
:
#include <Windows.h>
bool TryLock(__int64* plock)
{
__int64 lock = *plock;
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
Pokud chcete tento problém vyřešit, vynuťte volatile
čtení tak, aby kompilátor neoptimalizoval kód tak, aby po sobě četl ze stejné paměti, pokud není explicitně instruován. Tím zabráníte optimalizátoru v zavedení neočekávaného chování.
První metoda, která bude považovat paměť za volatile
cílovou adresu jako volatile
ukazatel:
#include <Windows.h>
bool TryLock(volatile __int64* plock)
{
__int64 lock = *plock;
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
Druhá metoda používá volatile
čtení z cílové adresy. Můžete to udělat několika různými způsoby:
- Přetypování ukazatele na
volatile
ukazatel před zrušením odvozování ukazatele - Vytvoření
volatile
ukazatele z poskytnutého ukazatele - Použití
volatile
pomocných funkcí pro čtení
Příklad:
#include <Windows.h>
bool TryLock(__int64* plock)
{
__int64 lock = ReadNoFence64(plock);
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
Heuristika
Toto pravidlo se vynucuje zjištěním, jestli se hodnota funkce InterlockedCompareExchange
Destination
nebo některé z jejích derivátů načte nečtenouvolatile
a pak se použije jako Comparand
hodnota. Nekontroluje ale explicitně, jestli se načtená hodnota používá k určení hodnoty výměny. Předpokládá, že hodnota výměny souvisí s Comparand
hodnotou.
Viz také
InterlockedCompareExchange
(winnt.h)
_InterlockedCompareExchange
vnitřní funkce