Sdílet prostřednictvím


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 InterlockedCompareExchangePointerje 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:

  1. Přečtěte si aktuální hodnotu z plock ukazatele.
  2. Zkontrolujte, jestli má tato aktuální hodnota nastavenou nejméně významnou bitovou sadu.
  3. 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é lockzásobníku . lock se používá třikrát:

  1. Nejprve zkontrolujte, jestli je nastavený nejméně významný bit.
  2. Za druhé, jako Comparand hodnota do InterlockedCompareExchange64.
  3. 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 InterlockedCompareExchange64a 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 volatileté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