DCL22-C. キャッシュできないデータには volatile を使う
volatile 修飾型オブジェクトは、処理系が関与しない方法で変更されたり、その他の未知の副作用を持つ可能性がある。たとえば、非同期シグナル処理が原因で、コンパイラが認識しない方法で、オブジェクトが変更されることがある。volatile 型修飾子をつけないと、意図せぬ最適化が行われることもある。これらの最適化は競合状態の原因になることがある。プログラマが競合状態を避けるコードを書いても、コンパイラがプログラマのデータモデルを認識せずコンパイル時にコードを変更して競合状態を招く可能性があるからである。
キーワード volatile
は、アクセスやキャッシュに制限をかけることで、このような混乱を防ぐ。C99 Rationale (理論的解釈) [ISO/IEC 2003] には、次のように記載されている。
この左辺値を介したキャッシュは行わないこと。抽象的意味での各演算が実行されなければならない(つまり、左辺値が置いてある位置に、以前の値が入っている保証はないため、キャッシュした値を使ってはいけない)。この修飾子がないと、エイリアシングによる場合を除き、指定された位置の内容は変わらないと想定される可能性がある。
オブジェクトを volatile として修飾する型は、複数のスレッド間の同期を保証せず、同時メモリアクセスに対する保護も行わない。また、sig_atomic_t
型のオブジェクトの宣言に使用されるのでなければ、オブジェクトへのアクセスのアトミック性も保証しない。シグナルハンドラ固有の制限については、「SIG31-C. シグナルハンドラ内で共有オブジェクトにアクセスしない」を参照。
違反コード (volatile
の欠落)
以下のコード例では、フラグを切り替えてループを終了させるために、SIGINT
シグナルを受け取ることを想定している。しかし、interrupted
に volatile
を宣言していないため、シグナルハンドラでの変数への割り当てにもかかわらず、main()
での interrupted
からの読み取りがコンパイラによる最適化で取り除かれループが終了しなくなる。たとえば、最適化のための -O
フラグを使用して GCC でコンパイルすると、プログラムは SIGINT
を受け取っても終了できない。
#include <signal.h> sig_atomic_t interrupted; /* バグ: volatile が宣言されていない */ void sigint_handler(int signum) { interrupted = 1; /* main() で割り当てが認識されない可能性がある */ } int main(void) { signal(SIGINT, sigint_handler); while (!interrupted) { /* ループが終了しない可能性がある */ /* 何かの処理 */ } return 0; }
適合コード
volatile
修飾子を変数宣言に追加することにより、while
ループのすべての繰り返しで、interrupted
が元のアドレスおよびシグナルハンドラ内からアクセスされることが保証される。
#include <signal.h> volatile sig_atomic_t interrupted; void sigint_handler(int signum) { interrupted = 1; } int main(void) { signal(SIGINT, sigint_handler); while (!interrupted) { /* 何かの処理 */ } return 0; }
sig_atomic_t
型は、非同期割り込みが存在する場合でも、1 つの不可分な実体としてアクセスできるオブジェクトの整数型である。sig_atomic_t
型は処理系定義であるが、SIG_ATOMIC_MIN
から SIG_ATOMIC_MAX
の範囲の整数値を安全に格納できることの保証は提供している。また、sig_atomic_t
が符号付き整数型の場合、SIG_ATOMIC_MIN
は -127
以下でなければならず、SIG_ATOMIC_MAX
は 127
以上でなければならない。それ以外の場合では、SIG_ATOMIC_MIN
が 0
で SIG_ATOMIC_MAX
が 255 以上でなければならない。SIG_ATOMIC_MIN
マクロと SIG_ATOMIC_MAX
マクロは、<stdint.h>
ヘッダで定義される。
違反コード (volatile
へのキャスト)
以下のコード例では、thread_func
関数が、グローバル変数 end_processing
を介して相互通信するように設計されている複数のスレッドのコンテキストで実行される。関数は、アクセス前に変数を volatile にキャストすることで、コンパイラが最適化を行って while
ループ条件を取り除くことを防止しようとしている。しかし、end_processing
が volatile に宣言されていないため、ループ本体内での割り当てをレジスタからメモリにフラッシュする必要がなくなり、キャストにもかかわらず、読み取り時に割り当てが認識されない。その結果、ループが終了しないことがある。
extern int compute(void*); static _Bool end_processing; void* thread_func(void *arg) { while (0 == *(volatile _Bool*)&end_processing) { int status; status = compute(arg); if (status) { /* 他のスレッドに処理の終了を通知する */ end_processing = 1; break; } } return 0; }
適合コード
volatile
修飾子を変数宣言に追加することにより、while
ループのすべての繰り返しで、end_processing
が元のアドレスの読み書きを行うことが保証される。
extern int compute(void*); static volatile _Bool end_processing; void* thread_func(void *arg) { while (0 == end_processing) { int status; status = compute(arg); if (status) { /* 他のスレッドに処理の終了を通知する */ end_processing = 1; break; } } return 0; }
しかし、2 つ以上の実行スレッドからオブジェクトに同時アクセスが行われる場合、データ競合を防止するには、オブジェクトに volatile を宣言するだけでは不十分であることに注意。たとえば、各スレッドが別々のプロセッサ上で実行されている場合、メモリ可視性制限を追加して、メモリバリアなどの、プラットフォーム固有の構成を使用しなければならない場合がある。詳細は、CON02-C. Do not use volatile as a synchronization primitive を参照。
リスク評価
volatile
修飾子を使用しないと、コードの非同期部分に競合状態が発生する可能性がある。これにより、予期せぬ値が格納され、データの完全性が侵害される可能性がある。
レコメンデーション |
深刻度 |
可能性 |
修正コスト |
優先度 |
レベル |
---|---|---|---|---|---|
DCL22-C |
低 |
中 |
高 |
P2 |
L3 |
自動検出(最新の情報はこちら)
ツール |
バージョン |
チェッカー |
説明 |
---|---|---|---|
PRQA QA-C | 8.1 | 2782 | 部分的に実装済み |
関連するガイドライン
CERT C++ Secure Coding Standard | DCL34-CPP. Use volatile for data that cannot be cached |
参考資料
[ISO/IEC 2003] | Section 6.7.3, "Type Qualifiers" |
[Sun 2005] | Chapter 6, "Transitioning to ISO C" |
翻訳元
これは以下のページを翻訳したものです。
DCL22-C. Use volatile for data that cannot be cached (revision 78)