Поделиться через


Предупреждение C26837

Значение для функции сравнения comp func было загружено из расположения назначения 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. Во-вторых, в качестве Comparand значения InterlockedCompareExchange64.
  3. Наконец, в сравнении возвращаемого значения из InterlockedCompareExchange64

В этом случае предполагается, что текущее значение, сохраненное в переменной стека, считывается сразу в начале функции и не изменяется. Это необходимо, так как текущее значение сначала проверяется перед попыткой операции, а затем явно используется в качестве Comparand входного InterlockedCompareExchange64и, наконец, используется для сравнения возвращаемого значения.InterlockedCompareExchange64

К сожалению, предыдущий код можно скомпилировать в сборку, которая ведет себя не так, как ожидалось от исходного кода. Скомпилируйте предыдущий код с помощью компилятора Microsoft Visual C++ (MSVC) и /O1 проверьте результирующий код сборки, чтобы узнать, как получается значение блокировки для каждой ссылки lock . Версия компилятора MSVC версии 19.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 параметром, что и раньше. Созданная сборка больше не считывается plock для использования кэшированного значения в lock.

Дополнительные примеры исправления кода см. в разделе "Пример".

Имя анализа кода: INTERLOCKED_COMPARE_EXCHANGE_MISUSE

Пример

Компилятор может оптимизировать следующий код для многократного чтения plock вместо использования кэшированного значения в lock:

#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; 
}

Эвристика

Это правило применяется путем определения того, загружается ли значение в Destination InterlockedCompareExchange функции или любой из производных функций через нечитаемыйvolatile , а затем используется в качестве Comparand значения. Однако он не проверяет, используется ли загруженное значение для определения значения обмена . Предполагается, что значение обмена связано со значением Comparand .

См. также

InterlockedCompareExchange function (winnt.h)
_InterlockedCompareExchange встроенные функции