整数変数は多くの場合、数値やビット集合を表現することを意図して用いられる。数値であれば算術演算のみを行い、ビットの集合であればビット演算を行うべきである。ビット演算子には単項演算子の~があり、二項演算子には、<<、>>、>>>、&、^、|がある。
同一のデータに対してビット演算と算術演算の両方を行うと、変数に格納されているデータが数値を意図しているのかビット集合を意図しているのかが分からなくなるし、データの意図に混乱が生じていることの徴候であることが多い。あいにくビット演算は、数値データに対する尚早な最適化(premature optimization)として使われることも多い。このように演算子を使用しても、コードとしては有効でありコンパイルは通るが、可読性は低下する。
違反コード (左シフト)
左シフトおよび右シフト演算子はしばしば、2のべき乗の乗算や除算を行うために使われる。これは実質的には高速化にはならず、かえってコードの可読性や可搬性が低下することが多い。Java仮想マシンは通常、このような最適化を自動的に行うが、プログラマと違い、プログラムが実行されるプラットフォームに合った最適化を行うことができる。以下の違反コード例では、数値を保持することを意図した整数変数xに対して、ビット単位の演算と数値演算の両方を行っている。結果として得られる尚早な最適化が行われた式では、プログラマの意図を反映して、5x + 1という値をxに代入している。
int x = 50; x += (x << 2) + 1;
違反コード (左シフト)
次の違反コード例では、変数ごとにビット演算子と算術演算子を分けている。変数 x はビット演算のみに使用され、変数 y は算術演算のみに使用されている。
int x = 50; int y = x << 2; y += y + 1;
演算自体は異なる変数に対して行われているが、実際のデータに対してはビット単位の演算と算術演算の両方が行われているため、このコード例はルールに違反している。
適合コード (左シフト)
以下の適合コードでは、数値であるxの性質を反映するように代入式を変更することで、プログラマの意図をより明確に表現している。
int x = 50; x = 5 * x + 1;
この変更により、演算が整数オーバフローを引き起こさないように検査すべきであることがコードレビュー担当者に明らかとなる。これは、元の違反コードでは明らかではなかった。「NUM00-J. 整数オーバーフローを検出あるいは防止する」も参照のこと。
違反コード (論理右シフト)
以下の違反コード例では、プログラマは x を 4 で割ることを意図している。パフォーマンスを最適化しようとして、除算演算の代わりに右シフトを使っている。
int x = -50; x >>>= 2;
>>>= 演算子は論理右シフト演算である。シフトする値の符号に関係なく、左側からビットゼロを詰める。このコードを実行すると、x は非常に大きな正の値になる(具体的には0x3FFFFFF3)。被除数(この例では x)が負の値である場合、除算演算の代わりに論理右シフトを使うと、計算結果は間違った値になる。
違反コード (算術右シフト)
以下の違反コード例では、前述の違反コード例の間違いを、算術右シフトを使うことで修正しようとしている。
int x = -50; x >>= 2;
このコードの実行後、x の値は -12 ではなく -13 となる。算術右シフトでは計算結果は負の無限大の方向に切り捨てられる。一方、整数除算ではゼロに向かって切り捨てられる。
適合コード (右シフト)
以下の適合コードでは、右シフトを除算に置き換えている。
int x = -50; x /= 4;
違反コード
以下の違反コード例は、バイト配列から4つの値を取り出し、それらの値を整数変数 result に詰め込むことを意図している。このコード例における整数値は、数値ではなく、ビットの集まりを表現している。
// b[] は値 0xff に初期化されたバイト配列 byte[] b = new byte[] {-1, -1, -1, -1}; int result = 0; for (int i = 0; i < 4; i++) { result = ((result << 8) + b[i]); }
ビット演算において、バイト配列の要素 b[i] の値は 符号拡張によって int に格上げされる。もしバイト配列の要素に負の値(たとえば 0xff)が入っていると、符号拡張により、符号ビットの'1'がintの上位24ビットに伝播する。byte が符号無し型であると想定している場合、この動作は予期せぬものであろう。このコード例では、格上げされた byte 値を result に加算するところで、バイト値の並びをintに詰め込んだ表現になっていない[FindBugs 2008]。
違反コード
以下の違反コード例では、加算を行う前に、格上げしたバイト配列の要素の上位24ビットをマスクしている。byte と int をマスクするために必要なビット数は Java 言語仕様で規定されている。このコードは正しい結果を計算するが、同一のデータにビット演算と算術演算を行っている点でルールに違反している。
byte[] b = new byte[] {-1, -1, -1, -1}; int result = 0; for (int i = 0; i < 4; i++) { result = ((result << 8) + (b[i] & 0xff)); }
適合コード
以下の適合コードでは、格上げしたバイト配列の要素の上位24ビットをマスクし、その計算結果を result の値と論理 OR 演算している。
byte[] b = new byte[] {-1, -1, -1, -1}; int result = 0; for (int i = 0; i < 4; i++) { result = ((result << 8) | (b[i] & 0xff)); }
例外
NUM01-EX0: 定数式を構成するためであれば、ビット演算を使ってもよい。
int limit = (1 << 17) - 1; // 2^17 - 1 = 131071
コーディングスタイル上の問題ではあるが、上記のような定数式よりは16進定数として記述する方が望ましい。
int limit = 0x1FFFF; // 2^17 - 1 = 131071
NUM01-EX1: 通常は算術的に扱われるデータであっても、シリアライズ(serialization)や復元(deserialization)の際にはビット演算を行ってよい。これはファイルやネットワークソケットからのデータ読み書きで必要となることが多い。また、バイトデータにパックされたデータの読み書きにおいてもビット演算が許される。
int value = /* 値を初期化 */ Byte[] bytes = new Byte[4]; for (int i = 0; i < bytes.length; i++) { bytes[i] = value >> (i*8) & 0xFF; } /* bytes[] は value と同じビット表現になっている */
リスク評価
同一の変数に対してビット演算と算術演算の両方を行うと、プログラマの意図が分かりにくくなり、コードの可読性が低下する。また、セキュリティ監査担当者やコードの保守担当者にとっては、脆弱性を排除し、データの完全性を保証するためにどのような検査を行うべきか判断することが困難になる。たとえば、オーバフローのチェックは、数値演算を適用する数値データに対しては重要であるが、ビット演算のみを行う数値データに対してはそれほど重要ではない。
ルール | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
NUM01-J | 中 | 低 | 中 | P4 | L3 |
関連ガイドライン
CERT C Secure Coding Standard | INT14-C. Avoid performing bitwise and arithmetic operations on the same data |
CERT C++ Secure Coding Standard | INT14-CPP. Avoid performing bitwise and arithmetic operations on the same data |
参考文献
[Steele 1977] |
翻訳元
これは以下のページを翻訳したものです。
NUM01-J. Do not perform bitwise and arithmetic operations on the same data (revision 74)