다음을 통해 공유


경고 C26837

함수 func에 대한 피비교수 comp의 값이 비휘발성 읽기를 통해 대상 위치 dest에서 로드되었습니다.

이 규칙은 Visual Studio 2022 17.8에 추가되었습니다.

설명

InterlockedCompareExchange 함수와 InterlockedCompareExchangePointer와 같은 해당 파생 항목은 지정된 값에 대해 원자적 비교 및 교환 작업을 수행합니다. Destination 값이 Comparand 값과 같은 경우 교환 값은 Destination에서 지정한 주소에 저장됩니다. 그렇지 않으면 작업이 수행되지 않습니다. interlocked 함수는 여러 스레드에서 공유하는 변수에 대한 액세스를 동기화하기 위한 간단한 메커니즘을 제공합니다. 이 함수는 다른 interlocked 함수에 대한 호출과 관련하여 원자적입니다. 이러한 함수의 오용은 최적화가 예기치 않은 방식으로 코드의 동작을 변경할 수 있기 때문에 예상과 다르게 동작하는 개체 코드를 생성할 수 있습니다.

다음 코드를 생각해 봅시다.

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

이 코드의 의도는 다음과 같습니다.

  1. plock 포인터에서 현재 값을 읽습니다.
  2. 이 현재 값에 최하위 비트가 설정되어 있는지 확인합니다.
  3. 최하위 비트가 설정된 경우 현재 값의 다른 비트를 유지하면서 비트를 지웁니다.

이 작업을 달성하기 위해 plock 포인터에서 현재 값의 복사본을 읽고 스택 변수 lock에 저장합니다. lock은(는) 세 번 사용됩니다.

  1. 먼저 최하위 비트가 설정되어 있는지 확인하기 위해 사용됩니다.
  2. 둘째, InterlockedCompareExchange64에 대한 Comparand 값으로 사용됩니다.
  3. 마지막으로 InterlockedCompareExchange64의 반환 값을 비교하는 데 사용됩니다.

이렇게 하면 스택 변수에 저장된 현재 값을 함수 시작 시 한 번 읽고 변경되지 않는다고 가정합니다. 작업을 시도하기 전에 현재 값을 먼저 확인한 다음 InterlockedCompareExchange64에서 Comparand(으)로 명시적으로 사용하고 마지막으로 InterlockedCompareExchange64의 반환 값을 비교하는 데 사용되기 때문에 이 작업이 필요합니다.

아쉽게도 이전 코드는 소스 코드에서 예상한 것과 다르게 동작하는 어셈블리로 컴파일할 수 있습니다. MSVC(Microsoft Visual C++) 컴파일러 및 /O1 옵션을 사용하여 이전 코드를 컴파일하고 결과 어셈블리 코드를 검사하여 lock에 대한 각 참조에 대한 잠금 값을 얻는 방법을 확인합니다. MSVC 컴파일러 버전 v19.37은 다음과 같은 어셈블리 코드를 생성합니다.

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은(는) 매개 변수 plock의 값을 보유합니다. 어셈블리 코드는 스택에서 현재 값의 복사본을 만드는 대신 매번 plock 값을 다시 읽습니다. 즉, 읽을 때마다 값이 달라질 수 있습니다. 이렇게 하면 개발자가 수행하는 삭제가 무효화됩니다. 값에 최하위 비트가 설정되어 있는지 확인한 후 plock에서 다시 읽습니다. 이 유효성 검사가 수행된 후에 값을 다시 읽기 때문에 새 값에 더 이상 최하위 비트가 설정되지 않을 수 있습니다. 경합 상태에서 이 코드는 다른 스레드에 의해 이미 잠겼을 때 지정된 잠금을 성공적으로 얻은 것처럼 동작할 수 있습니다.

컴파일러는 코드 동작이 변경되지 않는 한 메모리 읽기 또는 쓰기를 제거하거나 추가할 수 있습니다. 컴파일러가 이러한 변경을 수행하는 것을 방지하려면 메모리에서 값을 읽을 때 읽기가 volatile이(가) 되도록 강제하고 변수에 캐시합니다. volatile(으)로 선언된 개체는 값이 언제든지 변경될 수 있으므로 특정 최적화에 사용되지 않습니다. 생성된 코드는 이전 명령에서 동일한 개체의 값을 요청했더라도 요청될 때 항상 volatile 개체의 현재 값을 읽습니다. 반대의 경우도 같은 이유로 적용됩니다. 요청하지 않는 한 volatile 개체의 값은 다시 읽혀지지 않습니다. volatile에 대한 자세한 내용은 volatile을 참조하세요. 예시:

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

이전과 동일한 /O1 옵션을 사용하여 이 코드를 컴파일합니다. 생성된 어셈블리는 더 이상 lock에 캐시된 값을 사용하기 위해 plock을(를) 읽지 않습니다.

코드를 수정하는 방법에 대한 추가 예제는 예제를 참조하세요.

코드 분석 이름: INTERLOCKED_COMPARE_EXCHANGE_MISUSE

예시

컴파일러는 lock에 캐시된 값을 사용하는 대신 plock을(를) 여러 번 읽도록 다음 코드를 최적화할 수 있습니다.

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

문제를 해결하려면 컴파일러가 명시적으로 지시하지 않는 한 동일한 메모리에서 연속적으로 읽도록 코드를 최적화하지 않도록 읽기가 volatile이(가) 되도록 강제합니다. 이렇게 하면 최적화 프로그램에서 예기치 않은 동작이 발생하지 않도록 할 수 있습니다.

메모리를 volatile(으)로 취급하는 첫 번째 방법은 대상 주소를 volatile 포인터로 사용하는 것입니다.

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

두 번째 방법은 대상 주소에서 읽은 volatile을(를) 사용하는 것입니다. 이 작업을 수행하는 몇 가지 방법이 있습니다.

  • 포인터를 역참조하기 전에 포인터를 volatile 포인터로 캐스팅
  • 제공된 포인터에서 volatile 포인터 만들기
  • volatile 읽기 도우미 함수를 사용합니다.

예시:

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

경험적 학습

이 규칙은 InterlockedCompareExchange 함수의 Destination 값 또는 해당 파생 항목이 volatile이(가) 아닌 읽기를 통해 로드된 다음 Comparand 값으로 사용되는지 감지하여 적용됩니다. 그러나 로드된 값이 교환 값을 확인하는 데 사용되는지 명시적으로 확인하지는 않습니다. 교환 값이 Comparand 값과 관련이 있다고 가정합니다.

참고 항목

InterlockedCompareExchange 함수(winnt.h)
_InterlockedCompareExchange 내장 함수