Aviso C26837
O valor do comparando
comp
para a funçãofunc
foi carregado do local de destinodest
por meio de leitura não volátil.
A regra foi adicionada no Visual Studio 2022 17.8.
Comentários
A função InterlockedCompareExchange
e seus derivados, como InterlockedCompareExchangePointer
, executam uma operação atômica de comparação e troca nos valores especificados. Se o valor Destination
for igual ao valor Comparand
, o valor de troca será armazenado no endereço especificado por Destination
. Caso contrário, nenhuma operação será executada. As funções interlocked
fornecem um mecanismo simples para sincronizar o acesso a uma variável compartilhada por vários threads. Essa função é atômica em relação a chamadas para outras funções interlocked
. O uso indevido dessas funções pode gerar código de objeto que se comporta de forma diferente do esperado, pois a otimização pode alterar o comportamento do código de maneiras inesperadas.
Considere o seguinte código:
#include <Windows.h>
bool TryLock(__int64* plock)
{
__int64 lock = *plock;
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
A intenção desse código é:
- Leia o valor atual do ponteiro
plock
. - Verifique se esse valor atual tem o conjunto de bits menos significativo.
- Se ele tiver um conjunto de bits menos significativo, desmarque o bit preservando os outros bits do valor atual.
Para fazer isso, uma cópia do valor atual é lida do ponteiro plock
e salva em uma variável de pilha lock
. lock
é usado três vezes:
- Primeiro, para verificar se o conjunto de bits menos significativo está definido.
- Em segundo lugar, como o valor
Comparand
paraInterlockedCompareExchange64
. - Por fim, na comparação do valor de retorno de
InterlockedCompareExchange64
Isso pressupõe que o valor atual salvo na variável de pilha é lido uma vez no início da função e não se altera. Isso é necessário porque o valor atual é verificado primeiro antes de tentar a operação, depois usado explicitamente como o Comparand
em InterlockedCompareExchange64
e, por fim, usado para comparar o valor de retorno de InterlockedCompareExchange64
.
Infelizmente, o código anterior pode ser compilado em assembly, que se comporta de forma diferente do esperado do código-fonte. Compile o código anterior com o compilador do Microsoft Visual C++ (MSVC) e a opção /O1
e inspecione o código de assembly resultante para ver como o valor do bloqueio para cada uma das referências a lock
é obtido. O compilador MSVC versão v19.37 produz um código de assembly com a seguinte aparência:
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
contém o valor do parâmetro plock
. Em vez de fazer uma cópia do valor atual na pilha, o código do assembly está lendo novamente o valor de plock
todas as vezes. Isso significa que o valor pode ser diferente sempre que for lido. Isso invalida a sanitização que o desenvolvedor está executando. O valor é lido novamente de plock
depois que é verificado que ele tem seu conjunto de bits menos significativo. Como ele é lido novamente depois que essa validação é executada, o novo valor pode não ter mais o conjunto de bits menos significativo. Em uma condição de corrida, esse código pode se comportar como se tivesse obtido com sucesso o bloqueio especificado quando ele já estava bloqueado por outro thread.
O compilador tem permissão para remover ou adicionar leituras ou gravações de memória, desde que o comportamento do código não seja alterado. Para impedir que o compilador faça essas alterações, force as leituras a serem volatile
quando você ler o valor da memória e o armazená-lo em cache em uma variável. Objetos que são declarados como volatile
não são usados em determinadas otimizações porque seus valores podem ser alterados a qualquer momento. O código gerado sempre lê o valor atual de um objeto volatile
quando solicitado, mesmo que uma instrução anterior solicite um valor do mesmo objeto. O inverso também se aplica pelo mesmo motivo. O valor do objeto volatile
não é lido novamente, a menos que solicitado. Para obter mais informações sobre volatile
, consulte volatile
. Por exemplo:
#include <Windows.h>
bool TryLock(__int64* plock)
{
__int64 lock = *static_cast<volatile __int64*>(plock);
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
Compile esse código com a mesma opção /O1
de antes. O assembly gerado não lê mais plock
para uso do valor armazenado em cache em lock
.
Para obter mais exemplos de como o código pode ser corrigido, consulte Exemplo.
Nome da análise de código: INTERLOCKED_COMPARE_EXCHANGE_MISUSE
Exemplo
O compilador pode otimizar o código a seguir para ler plock
várias vezes em vez de usar o valor armazenado em cache em lock
:
#include <Windows.h>
bool TryLock(__int64* plock)
{
__int64 lock = *plock;
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
Para corrigir o problema, force as leituras a serem volatile
para que o compilador não otimize o código para leitura sucessiva da mesma memória, a menos que seja explicitamente instruído. Isso impede que o otimizador introduza um comportamento inesperado.
O primeiro método para tratar a memória como volatile
é usar o endereço de destino como ponteiro volatile
:
#include <Windows.h>
bool TryLock(volatile __int64* plock)
{
__int64 lock = *plock;
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
O segundo método está usando volatile
lido do endereço de destino. Há algumas maneiras diferentes de fazer isso:
- Lançando o ponteiro para ponteiro
volatile
antes de desreferenciar o ponteiro - Criando um ponteiro
volatile
do ponteiro fornecido - Usando funções auxiliares de leitura
volatile
.
Por exemplo:
#include <Windows.h>
bool TryLock(__int64* plock)
{
__int64 lock = ReadNoFence64(plock);
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
Heurística
Essa regra é imposta detectando se o valor no Destination
da função InterlockedCompareExchange
ou qualquer um de seus derivados é carregado por meio de uma leitura não-volatile
e, em seguida, usado como o valor Comparand
. No entanto, ele não verifica explicitamente se o valor carregado é usado para determinar o valor de troca. Ele pressupõe que o valor de troca esteja relacionado ao valor Comparand
.
Confira também
InterlockedCompareExchange
função (winnt.h)
Funções intrínsecas _InterlockedCompareExchange