1. ホーム
  2. c++

C/C++で符号なし左シフトの前にマスクするのは偏執的すぎる?

2023-09-20 10:35:10

質問

この質問は、私が C/C++ で暗号アルゴリズム (例: SHA-1) を実装し、プラットフォームに依存しない移植可能なコードを書き、そして徹底的に 未定義の動作 .

標準化された暗号アルゴリズムから、これを実装するよう求められたとします。

b = (a << 31) & 0xFFFFFFFF

ここで ab は符号なし32ビット整数である。結果において、最下位32ビットより上のビットはすべて破棄されていることに注意してください。


最初の素朴な近似として、私たちは次のように仮定するかもしれません。 int はほとんどのプラットフォームで 32 ビット幅であると仮定し、次のように記述します。

unsigned int a = (...);
unsigned int b = a << 31;

このコードがどこでも使えるわけではないことは、次の理由からわかります。 int はあるシステムでは 16 ビット幅、他のシステムでは 64 ビット幅、そしておそらく 36 ビット幅であるためです。しかし stdint.h を使えば、このコードを改善することができます。 uint32_t 型に変更することができます。

uint32_t a = (...);
uint32_t b = a << 31;

では、もういいんですね?私は何年もそう思っていたのですが ... そうでもないんです。あるプラットフォームで、あるとします。

// stdint.h
typedef unsigned short uint32_t;

C/C++で算術演算を行う際のルールとして、型(例えば short よりも狭い場合 int よりも狭い場合、その幅は int に拡大され、すべての値が収まる場合は unsigned int でなければ

例えば、コンパイラが定義した short を32ビット(符号付き)と定義し int を48ビット(符号付き)とする。すると、このようなコード行になります。

uint32_t a = (...);
uint32_t b = a << 31;

は事実上、意味することになります。

unsigned short a = (...);
unsigned short b = (unsigned short)((int)a << 31);

なお aint に昇格します。 ushort (すなわち uint32 ) に適合するのは int (すなわち int48 ).

しかし、今度は問題が発生しました。 符号付き整数型の符号ビットに非ゼロビットを左シフトすることは、未定義の動作です。 . この問題が発生したのは、私たちの uint32 に昇格したためです。 int48 - に昇格する代わりに uint48 (に昇格するのではなく(左遷でOKなところ)。


以下は私の質問です。

  1. 私の推論は正しく、これは理論的には正当な問題なのでしょうか?

  2. すべてのプラットフォームで、次の整数型は幅が 2 倍であるため、この問題は無視しても安全ですか?

  3. このような入力を事前にマスクすることによって、この病的な状況を正しく防御することは良いアイデアでしょうか? b = (a & 1) << 31; . (これは必ずしもすべてのプラットフォームで正しいでしょう。しかし、これでは速度が重要な暗号アルゴリズムが必要以上に遅くなってしまう可能性があります)。

明確化/編集しました。

  • C か C++、あるいはその両方についての回答を受け付けます。少なくともどちらかの言語の答えが知りたいのです。

  • プリマスクロジックはビット回転を損なう可能性があります。例えば、GCCはコンパイル時に b = (a << 31) | (a >> 1); をアセンブリ言語の32ビットビット・ローテーション命令にコンパイルします。しかし、左シフトを事前にマスクすると、新しいロジックがビット回転に変換されない可能性があり、これは、今、1ではなく、4つのオペレーションが実行されることを意味します。

どのように解決するのですか?

ヒントとなるのは この質問 の UB の可能性について uint32 * uint32 の算術演算について、C や C++では次のような簡単な方法で動作するはずです。

uint32_t a = (...);
uint32_t b = (uint32_t)((a + 0u) << 31);

整数の定数 0u は型 unsigned int . これにより a + 0uuint32_t または unsigned int のどちらか広い方です。型はランクを持つので int 以上であるため、これ以上の昇格は発生せず、シフトは左オペランドが uint32_t または unsigned int .

への最後のキャストは uint32_t は単に狭窄変換に関する潜在的な警告を抑制するだけです (例えば int が64ビットの場合)。

まともなCコンパイラーは、ゼロを追加することは無意味であり、符号なしシフトの後にプリマスクが効果を持たないことを確認するよりも負担が少ないことを確認することができるはずです。