Compartir a través de


Advertencia C26837

El valor del comparando comp para la función func se ha cargado desde la ubicación de destino dest a través de una lectura no volátil.

Esta regla se agregó en Visual Studio 2022 17.8.

Comentarios

La función InterlockedCompareExchange y sus derivados como, por ejemplo, InterlockedCompareExchangePointer, realizan una operación atómica de comparación e intercambio en los valores especificados. Si el valor Destination es igual al valor Comparand, el valor de intercambio se almacena en la dirección especificada por Destination. De lo contrario, no se realiza ninguna operación. Las funciones de interlocked proporcionan un mecanismo sencillo para sincronizar el acceso a una variable compartida por varios subprocesos. Esta función es atómica respecto a las llamadas a otras funciones de interlocked. El uso incorrecto de estas funciones puede generar un código de objeto que se comporte de forma diferente a la esperada, ya que la optimización puede cambiar el comportamiento del código de maneras inesperadas.

Observe el código siguiente:

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

La intención del código es la siguiente:

  1. Leer el valor actual del puntero de plock.
  2. Comprobar si este valor actual tiene el bit menos significativo establecido.
  3. Si tiene el bit menos significativo establecido, borre el bit mientras conserva los demás bits del valor actual.

Para ello, se lee una copia del valor actual del puntero de plock y se guarda en una variable de pila lock. lock se usa tres veces:

  1. En primer lugar, para comprobar si se ha establecido el bit menos significativo.
  2. En segundo lugar, como el valor de Comparand para InterlockedCompareExchange64.
  3. Por último, en la comparación del valor de retorno de InterlockedCompareExchange64.

Aquí se supone que el valor actual guardado en la variable de pila se lee una vez al principio de la función y no cambia. Esto es necesario porque el valor actual se comprueba primero antes de intentar la operación; a continuación, se usa explícitamente como el Comparand en InterlockedCompareExchange64; y, por último, se usa para comparar el valor de retorno de InterlockedCompareExchange64.

Desafortunadamente, el código anterior se puede compilar en un ensamblado que se comporta de forma diferente a la esperada del código fuente. Compile el código anterior con el compilador de Microsoft Visual C++ (MSVC) y la opción /O1, e inspeccione el código de ensamblado resultante para ver cómo se obtiene el valor del bloqueo para cada una de las referencias a lock. La versión del compilador de MSVC v19.37 genera un código de ensamblado similar al siguiente:

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 mantiene el valor del parámetro plock. En lugar de realizar una copia del valor actual en la pila, el código de ensamblado vuelve a leer el valor en plock cada vez. Esto significa que el valor puede ser diferente cada vez que se lee. Como resultado, se invalida el saneamiento que está realizando el desarrollador. El valor se vuelve a leer en plock después de comprobar que tiene establecido el bit menos significativo. Como se vuelve a leer después de realizar esta validación, es posible que el nuevo valor ya no tenga establecido el bit menos significativo. En una condición de carrera, este código puede comportarse como si hubiera obtenido correctamente el bloqueo especificado cuando ya estaba bloqueado por otro subproceso.

El compilador tiene permiso para quitar o agregar lecturas o escrituras de memoria siempre que no se modifique el comportamiento del código. Para evitar que el compilador realice estos cambios, fuerce que las lecturas sean volatile cuando se lea el valor de la memoria y se almacene en caché en una variable. Los objetos declarados como volatile no se usan en ciertas optimizaciones porque sus valores pueden cambiar en cualquier momento. El código generado lee siempre el valor actual de un objeto volatile cuando se solicita, aunque una instrucción anterior pidiera un valor del mismo objeto. Lo contrario también se aplica por la misma razón. El valor del objeto volatile no se lee de nuevo a menos que se solicite. Para obtener más información sobre volatile, vea volatile. Por ejemplo:

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

Compile este código con la misma opción de /O1 que antes. El ensamblado generado ya no lee plock para usar el valor almacenado en la caché en lock.

Para ver más ejemplos de cómo se puede corregir el código, vea Ejemplo.

Nombre de análisis de código: INTERLOCKED_COMPARE_EXCHANGE_MISUSE

Ejemplo

El compilador puede optimizar el código siguiente para leer plock varias veces, en lugar de usar el valor almacenado en caché en lock:

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

Para corregir el problema, obligue a que las lecturas sean volatile, para que el compilador no optimice el código para leer sucesivamente en la misma memoria, a menos que se indique explícitamente. Esto impide que el optimizador introduzca un comportamiento inesperado.

El primer método para tratar la memoria como volatile es obtener la dirección de destino como un puntero volatile:

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

El segundo método es usar la lectura volatile de la dirección de destino. Hay varias formas de hacerlo:

  • Convertir el puntero en un puntero volatile antes de desreferenciar el puntero
  • Crear un puntero volatile a partir del puntero proporcionado
  • Utilizar las funciones del asistente de lectura volatile.

Por ejemplo:

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

Heurística

Esta regla se aplica detectando si el valor en el Destination de la función InterlockedCompareExchange, o cualquiera de sus derivados, se carga a través de una lectura no volatile y, a continuación, se usa como el valor de Comparand. Sin embargo, no comprueba explícitamente si se usa el valor cargado para determinar el valor de intercambio. Se supone que el valor de intercambio está relacionado con el valor de Comparand.

Consulte también

Función InterlockedCompareExchange (winnt.h)
_InterlockedCompareExchange (funciones intrínsecas)