Partilhar via


Aviso C26837

O valor do comparando comp para a função func foi carregado do local de destino dest 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 é:

  1. Leia o valor atual do ponteiro plock.
  2. Verifique se esse valor atual tem o conjunto de bits menos significativo.
  3. 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:

  1. Primeiro, para verificar se o conjunto de bits menos significativo está definido.
  2. Em segundo lugar, como o valor Comparand para InterlockedCompareExchange64.
  3. 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