Udostępnij za pośrednictwem


Ostrzeżenie C26837

Wartość comparand comp dla funkcji func została załadowana z lokalizacji dest docelowej za pośrednictwem nietrwałego odczytu.

Ta reguła została dodana w programie Visual Studio 2022 17.8.

Uwagi

Funkcja InterlockedCompareExchange i jej pochodne, takie jak InterlockedCompareExchangePointer, wykonują niepodzielne operacje porównywania i wymiany na określonych wartościach. Destination Jeśli wartość jest równa Comparand wartości, wartość wymiany jest przechowywana w adresie określonym przez Destination. W przeciwnym razie nie jest wykonywana żadna operacja. Funkcje interlocked zapewniają prosty mechanizm synchronizowania dostępu do zmiennej współużytkowanej przez wiele wątków. Ta funkcja jest niepodzielna w odniesieniu do wywołań do innych interlocked funkcji. Nieprawidłowe użycie tych funkcji może generować kod obiektu, który zachowuje się inaczej niż oczekiwano, ponieważ optymalizacja może zmienić zachowanie kodu w nieoczekiwany sposób.

Spójrzmy na poniższy kod:

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

Intencją tego kodu jest:

  1. Odczytaj bieżącą plock wartość ze wskaźnika.
  2. Sprawdź, czy ta bieżąca wartość ma najmniej znaczący zestaw bitów.
  3. Jeśli ma on najmniej znaczący zestaw bitów, wyczyść bit, zachowując pozostałe bity bieżącej wartości.

W tym celu kopia bieżącej wartości jest odczytywana ze plock wskaźnika i zapisywana w zmiennej stosu lock. lock jest używany trzy razy:

  1. Najpierw, aby sprawdzić, czy ustawiono najmniej znaczący bit.
  2. Po drugie, jako Comparand wartość na InterlockedCompareExchange64.
  3. Na koniec w porównaniu wartości zwracanej z InterlockedCompareExchange64

Przyjęto założenie, że bieżąca wartość zapisana w zmiennej stosu jest odczytywana raz na początku funkcji i nie zmienia się. Jest to konieczne, ponieważ bieżąca wartość jest najpierw sprawdzana przed podjęciem próby wykonania operacji, a następnie jawnie użyta jako Comparand w InterlockedCompareExchange64pliku , a na koniec użyta do porównania wartości zwracanej z InterlockedCompareExchange64.

Niestety poprzedni kod można skompilować do zestawu, który zachowuje się inaczej niż oczekiwano od kodu źródłowego. Skompiluj poprzedni kod za pomocą kompilatora języka Microsoft Visual C++ (MSVC) i /O1 sprawdź wynikowy kod zestawu, aby zobaczyć, jak wartość blokady dla każdego z odwołań do lock jest uzyskiwana. Kompilator MSVC w wersji 19.37 tworzy kod zestawu, który wygląda następująco:

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 przechowuje wartość parametru plock. Zamiast tworzyć kopię bieżącej wartości na stosie, kod zestawu ponownie odczytuje wartość za plock każdym razem. Oznacza to, że wartość może być inna za każdym razem, gdy jest odczytywana. Spowoduje to unieważnienie oczyszczania wykonywanego przez dewelopera. Wartość jest odczytywana ponownie po plock zweryfikowaniu, że ma ona najmniej znaczący zestaw bitów. Ponieważ jest on ponownie odczytywany po wykonaniu tej walidacji, nowa wartość może nie mieć już najmniej znaczącego zestawu bitów. W warunkach wyścigu ten kod może zachowywać się tak, jakby pomyślnie uzyskał określony blokadę, gdy został już zablokowany przez inny wątek.

Kompilator może usuwać lub dodawać odczyty lub zapisy pamięci, o ile zachowanie kodu nie zostanie zmienione. Aby zapobiec wprowadzeniu takich zmian przez kompilator, wymuś odczyty volatile , gdy odczytujesz wartość z pamięci i zapiszesz ją w pamięci w zmiennej. Obiekty zadeklarowane jako volatile nie są używane w niektórych optymalizacjach, ponieważ ich wartości mogą się zmieniać w dowolnym momencie. Wygenerowany kod zawsze odczytuje bieżącą wartość volatile obiektu, gdy jest on żądany, nawet jeśli poprzednia instrukcja zażądała wartości z tego samego obiektu. Odwrotnie dotyczy również tego samego powodu. Wartość volatile obiektu nie jest odczytywana ponownie, chyba że zażądano. Aby uzyskać więcej informacji na temat volatileprogramu , zobacz volatile. Na przykład:

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

Skompiluj ten kod z taką samą /O1 opcją jak poprzednio. Wygenerowany zestaw nie jest plock już odczytywany do użycia wartości buforowanej w pliku lock.

Aby uzyskać więcej przykładów sposobu naprawy kodu, zobacz Przykład.

Nazwa analizy kodu: INTERLOCKED_COMPARE_EXCHANGE_MISUSE

Przykład

Kompilator może zoptymalizować następujący kod do wielokrotnego odczytywania plock zamiast używania buforowanej wartości w pliku lock:

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

Aby rozwiązać ten problem, wymuś odczyty volatile tak, aby kompilator nie optymalizować kodu w celu odczytu z tej samej pamięci, chyba że jawnie zostanie poinstruowany. Zapobiega to nieoczekiwanemu zachowaniu optymalizatora.

Pierwszą metodą traktowania pamięci jako volatile jest użycie adresu docelowego jako volatile wskaźnika:

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

Druga metoda używa volatile odczytu z adresu docelowego. Istnieje kilka różnych sposobów, aby to zrobić:

  • Rzutowanie wskaźnika do volatile wskaźnika przed wyłuszczeniem wskaźnika
  • volatile Tworzenie wskaźnika na podstawie podanego wskaźnika
  • Korzystanie z volatile funkcji pomocnika odczytu.

Na przykład:

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

Algorytmy heurystyczne

Ta reguła jest wymuszana przez wykrycie, czy wartość w Destination funkcji lub którakolwiek z InterlockedCompareExchange jej pochodnych jest ładowana za pomocą nieczytanegovolatile , a następnie używanego Comparand jako wartość. Nie sprawdza jednak jawnie, czy załadowana wartość jest używana do określenia wartości wymiany . Przyjęto założenie, że wartość wymiany jest powiązana z wartością Comparand .

Zobacz też

InterlockedCompareExchange function (winnt.h)
_InterlockedCompareExchange funkcje wewnętrzne