我在 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
是因为所有的 ushort
(即 uint32
)都适合 int
(即 int48
)。
但是现在我们有一个问题: 将非零位左移到有符号整数类型的符号位是未定义行为的。发生这个问题是因为我们的 uint32
被提升到 int48
-而不是被提升到 uint48
(在那里左移是可以的)。
以下是我的问题:
我的推理是正确的吗? 这在理论上是一个合理的问题吗?
这个问题是否可以忽略,因为在每个平台上下一个整数类型是宽度的两倍?
通过像这样预先屏蔽输入来正确防御这种病态情况是一个好主意吗?校对: b = (a & 1) << 31;
。(这在每个平台上都必然是正确的。但这可能会使速度至关重要的加密算法变得不必要的慢。)
澄清/编辑:
我接受 C 或 C + + 或两者兼而有之的答案。我想知道至少一种语言的答案。
预掩蔽逻辑可能会损害位旋转。例如,GCC 将用汇编语言将 b = (a << 31) | (a >> 1);
编译成32位位旋转指令。但是如果我们预先屏蔽左移位,新逻辑可能不会转换成位旋转,这意味着现在要执行4个操作,而不是1个。