Ostrzeżenie C26837
Wartość comparand
comp
dla funkcjifunc
została załadowana z lokalizacjidest
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:
- Odczytaj bieżącą
plock
wartość ze wskaźnika. - Sprawdź, czy ta bieżąca wartość ma najmniej znaczący zestaw bitów.
- 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:
- Najpierw, aby sprawdzić, czy ustawiono najmniej znaczący bit.
- Po drugie, jako
Comparand
wartość naInterlockedCompareExchange64
. - 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 InterlockedCompareExchange64
pliku , 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 volatile
programu , 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