Advertencia C26837
El valor del comparando
comp
para la funciónfunc
se ha cargado desde la ubicación de destinodest
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:
- Leer el valor actual del puntero de
plock
. - Comprobar si este valor actual tiene el bit menos significativo establecido.
- 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:
- En primer lugar, para comprobar si se ha establecido el bit menos significativo.
- En segundo lugar, como el valor de
Comparand
paraInterlockedCompareExchange64
. - 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)