警告 C26837
関数
func
の比較対象comp
の値は、volatile でない読み取りを通じて、宛先の場所dest
から読み込まれています。
この規則は、Visual Studio 2022 17.8 で追加されました。
解説
InterlockedCompareExchange
関数とその派生関数 (InterlockedCompareExchangePointer
など) は、指定された値に対してアトミックな比較と交換の操作を実行します。 Destination
値が Comparand
値と等しい場合、exchange 値は Destination
によって指定されたアドレスに保存されます。 それ以外の場合は演算が実行されません。 interlocked
関数は、複数のスレッドによって共有される変数へのアクセスを同期するためのシンプルなメカニズムを提供します。 この関数は、他の interlocked
関数の呼び出しに関してアトミックです。 これらの関数の使用方法を誤ると、予期した動作とは異なる動作をするオブジェクト コードが生成される可能性があります。これは最適化によって予期しない方法でコードの動作が変更される可能性があるためです。
次のコードがあるとします。
#include <Windows.h>
bool TryLock(__int64* plock)
{
__int64 lock = *plock;
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
このコードの目的は以下のとおりです。
plock
ポインターから現在の値を読み取ります。- この現在の値に最下位ビットが設定されているかどうかを確認します。
- これに最下位ビットが設定されていない場合は、現在の値の他のビットを保持しながら該当ビットをクリアします。
これを実現するために、現在の値のコピーが plock
ポインターから読み取られ、スタック変数 lock
に保存されます。 lock
は以下のように 3 回使用されます。
- 最初は、最下位ビットが設定されているかどうかを確認するために。
- 2 回目は、
InterlockedCompareExchange64
に対するComparand
値として。 - 最後に、
InterlockedCompareExchange64
からの戻り値の比較において。
これは、スタック変数に保存されている現在の値が関数の開始時に一度読み取られ、変化しないことを前提としています。 これが必要な理由は、現在の値がまず操作の試みの前にチェックされた後、InterlockedCompareExchange64
の中で Comparand
として明示的に使用され、最後に InterlockedCompareExchange64
からの戻り値を比較するために使用されるためです。
残念ながら、上記のコードはソース コードから期待される動作とは異なる動作をするアセンブリにコンパイルされる可能性があります。 上記のコードを Microsoft Visual C++ (MSVC) コンパイラと /O1
オプションを使用してコンパイルし、結果のアセンブリ コードを調べて、lock
への参照のそれぞれで 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
として扱う 1 つ目の方法は、以下のように宛先アドレスを volatile
ポインターとして受け取ることです。
#include <Windows.h>
bool TryLock(volatile __int64* plock)
{
__int64 lock = *plock;
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
2 つ目の方法は、宛先アドレスからの 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
値として使用されるかどうかを検出することによって行われます。 しかし、読み込まれた値が exchange 値を特定するために使用されているかどうかは明示的にチェックしません。 exchange 値が Comparand
値に関連していることを前提としています。
関連項目
InterlockedCompareExchange
関数 (winnt.h)
_InterlockedCompareExchange
組み込み関数