web-dev-qa-db-ja.com

C / C ++の符号なし左シフトの前のマスキングは妄想的すぎますか?

この質問の動機は、C/C++で暗号化アルゴリズム(SHA-1など)を実装し、プラットフォームに依存しない移植可能なコードを記述し、 未定義の動作 を完全に回避することです。

標準化された暗号化アルゴリズムがこれを実装するように要求すると仮定します:

_b = (a << 31) & 0xFFFFFFFF
_

ここで、aおよびbは、符号なし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);
_

aはすべてint(つまり_uint32_)がushort(つまり_int48_)に収まるため、intに昇格されることに注意してください。

しかし今、問題があります:非ゼロビットを符号付き整数型の符号ビットにシフトすることは未定義の動作です。この問題は、_uint32_が_int48_に昇格する代わりに_uint48_に昇格したために発生しました(左シフトは問題ありません)。


私の質問は次のとおりです。

  1. 私の推論は正しいですか、これは理論上の正当な問題ですか?

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

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

明確化/編集:

  • C、C++、またはその両方の回答を受け入れます。少なくとも1つの言語の答えを知りたい。

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

71
Nayuki

この質問 から手掛かりを得て、uint32 * uint32算術の可能なUBについて、次の簡単なアプローチが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コンパイラーは、ゼロの追加は何もしないことを確認できるはずです。これは、符号なしシフトの後にプリマスクが効果を持たないことを確認するよりも面倒ではありません。

12
Nayuki

問題のC側に話すと、

  1. 私の推論は正しいですか、これは理論上の正当な問題ですか?

それは私が以前に考慮していなかった問題ですが、あなたの分析に同意します。 Cは、promoted左オペランドのタイプに関して<<演算子の動作を定義し、整数プロモーションの結果と考えられますその(署名された)intは、そのオペランドの元の型がuint32_tである場合。私は現代のマシンで実際にそれを見ることを期待していませんが、個人的な期待とは対照的に、私はすべて実際の標準に合わせてプログラミングするためのものです。

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

Cは、整数型間のこのような関係を必要としませんが、実際には遍在しています。ただし、標準のみに依存することに決めた場合、つまり厳密に適合するコードを書くことに苦労している場合は、そのような関係に依存することはできません。

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

タイプunsigned longは少なくとも32の値ビットを持つことが保証されており、整数プロモーションの下で他のタイプへのプロモーションの対象にはなりません。多くの一般的なプラットフォームでは、uint32_tとまったく同じ表現を持ち、同じ型である場合もあります。したがって、次のような式を書きたいと思います。

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

または、aの計算の中間値としてのみbが必要な場合は、最初にunsigned longとして宣言します。

24
John Bollinger

Q1:マスキングbeforeシフトは、OPが懸念する未定義の動作を防ぎます。

Q2:「...すべてのプラットフォームで、次の整数型は幅が2倍だから?」 ->いいえ。 「次の」整数型は、2x未満、または同じサイズです。

以下は、_uint32_t_を持つすべての準拠Cコンパイラに対して適切に定義されています。

_uint32_t a; 
uint32_t b = (a & 1) << 31;
_

Q3:uint32_t a; uint32_t b = (a & 1) << 31;には、マスクを実行するコードが発生することは想定されていません-実行可能ファイルではなく、ソースでのみ必要です。マスクが発生した場合、速度が問題になるはずのより良いコンパイラを取得します。

suggested のように、これらのシフトで符号なしであることを強調した方が良いでしょう。

_uint32_t b = (a & 1U) << 31;
_

@ John Bollinger OPの特定の問題を処理する方法を詳しく説明した良い回答。

generalの問題は、少なくともnビットである数を形成する方法であり、特定の符号性and驚くべき整数プロモーションの対象ではありません- OPのジレンマの中核。以下は、値を変更しないunsignedオペレーションを呼び出すことでこれを実現します-タイプの問題以外の効果的なノーオペレーション。製品は少なくともunsignedまたは_uint32_t_の幅になります。一般的に、キャストは型を狭める可能性があります。ナローイングが発生しないことが確実でない限り、キャストを避ける必要があります。最適化コンパイラは、不要なコードを作成しません。

_uint32_t a;
uint32_t b = (a + 0u) << 31;
uint32_t b = (a*1u) << 31;
_
19
chux

不要な昇格を避けるために、greater typeをtypedefとともに使用できます。

using my_uint_at_least32 = std::conditional_t<(sizeof(std::uint32_t) < sizeof(unsigned)),
                                              unsigned,
                                              std::uint32_t>;
10
Jarod42