C/C++ 中无符号左移前的掩码是否过于偏执?

Is masking before unsigned left shift in C/C++ too paranoid?

这个问题的动机是我在 C/C++ 中实现密码算法(例如 SHA-1),编写可移植的平台无关代码,并彻底避免 undefined behavior.

假设一个标准化的加密算法要求你实现这个:

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);

请注意,a 被提升为 int,因为所有 ushort(即 uint32)都适合 int(即 int48 ).

但现在我们遇到了一个问题:将非零位左移到有符号整数类型的符号位中是未定义的行为。发生这个问题是因为我们的 uint32 被提升为 int48 - 而不是被提升为 uint48 (左移就可以了)。


这是我的问题:

  1. 我的推理是否正确,理论上这是一个合理的问题吗?

  2. 忽略这个问题是否安全,因为在每个平台上下一个整数类型都是宽度的两倍?

  3. 通过像这样预先屏蔽输入来正确防御这种病态情况是个好主意吗?:b = (a & 1) << 31;。 (这在每个平台上都必然是正确的。但这可能会使速度关键的加密算法比必要的慢。)

Clarifications/edits:

为避免不必要的提升,您可以使用带有一些类型定义的 greater 类型,如

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

对于这段代码:

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

要将 a 提升为无符号类型而不是有符号类型,请使用:

uint32_t b = a << 31u;

<<运算符两边都是无符号类型时,则适用6.3.1.8(C标准草案n1570)中的这一行:

Otherwise, if both operands have signed integer types or both have unsigned integer types, the operand with the type of lesser integer conversion rank is converted to the type of the operand with greater rank.


您所描述的问题是由于您使用了 31,即 signed int type,所以 6.3.1.8 中的另一行

Otherwise, if the type of the operand with signed integer type can represent all of the values of the type of the operand with unsigned integer type, then the operand with unsigned integer type is converted to the type of the operand with signed integer type.

强制a提升为签名类型


更新:

这个答案不正确,因为 6.3.1.1(2)(强调我的):

...

If an int can represent all values of the original type (as restricted by the width, for a bit-field), the value is converted to an int; otherwise, it is converted to an unsigned int. These are called the integer promotions.58) All other types are unchanged by the integer promotions.

和脚注 58(强调我的):

58) The integer promotions are applied only: as part of the usual arithmetic conversions, to certain argument expressions, to the operands of the unary +, -, and ~ operators, and to both operands of the shift operators, as specified by their respective subclauses.

由于只发生整数提升而不是常见的算术转换,使用 31u 并不能保证 a 如上所述转换为 unsigned int

Q1:在 之前 掩蔽确实可以防止 OP 关注的未定义行为。

Q2:“...因为在每个平台上下一个整数类型都是宽度的两倍?” --> 不。 "next" 整数类型可以小于 2x 甚至相同大小。

以下是为所有具有 uint32_t.

的兼容 C 编译器明确定义的
uint32_t a; 
uint32_t b = (a & 1) << 31;

Q3:uint32_t a; uint32_t b = (a & 1) << 31; 预计不会产生执行掩码的代码 - 可执行文件中不需要它 - 仅在源代码中需要。如果确实出现掩码,获得更好的编译器应该是速度问题。

一样,最好强调这些移位的无符号性。

uint32_t b = (a & 1U) << 31;

很好的回答详细说明了如何处理 OP 的具体问题。

一般问题是如何形成一个至少有n位、特定符号和[=40的数字=] 不受令人惊讶的整数促销的影响 - OP 困境的核心。下面通过调用不更改值的 unsigned 操作来实现这一点 - 除了类型问题之外,有效的无操作。产品的宽度 至少 unsigneduint32_t。一般来说,铸造可能会缩小类型。除非确定不会发生变窄,否则需要避免铸造。优化编译器不会创建不必要的代码。

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

说到问题的C端,

  1. Is my reasoning correct, and is this a legitimate problem in theory?

这是我以前没有考虑过的问题,但我同意你的分析。 C 根据 promoted 左操作数的类型定义 << 运算符的行为,并且可以想象整数提升导致(有符号)int 当该操作数的原始类型为 uint32_t 时。我不希望在任何现代机器上实际看到这一点,但我完全赞成按照实际标准进行编程,而不是我个人的期望。

  1. Is this problem safe to ignore because on every platform the next integer type is double the width?

C 不需要整数类型之间的这种关系,尽管它在实践中无处不在。但是,如果您决心只依赖标准——也就是说,如果您正在努力编写严格符合标准的代码——那么您就不能依赖这种关系。

  1. Is a good idea to correctly defend against this pathological situation by pre-masking the input like this?: b = (a & 1) << 31;. (This will necessarily be correct on every platform. But this could make a speed-critical crypto algorithm slower than necessary.)

类型 unsigned long 保证至少有 32 个值位,并且在整数提升下它不受提升为任何其他类型的影响。在许多常见平台上,它与 uint32_t 具有完全相同的表示形式,甚至可能是同一类型。因此,我倾向于这样写表达式:

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

或者如果您只需要 a 作为计算 b 的中间值,则首先将其声明为 unsigned long

this question 中获取关于 uint32 * uint32 算法中可能的 UB 的线索,以下简单方法应该适用于 C 和 C++:

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

整数常量 0u 的类型为 unsigned int。这促进了将 a + 0u 添加到 uint32_tunsigned int,以更宽的为准。因为该类型的等级为 int 或更高,所以不再发生提升,并且可以在左操作数为 uint32_tunsigned int.

的情况下应用移位

最终转换回 uint32_t 只会抑制有关缩小转换的潜在警告(假设 int 是 64 位)。

一个体面的 C 编译器应该能够看到加零是一个空操作,这比看到一个无符号移位后预掩码没有效果要容易得多。