这个问题的动机是我在C / C ++中实现加密算法(例如SHA-1),编写可移植平台无关的代码,并彻底避免使用undefined behavior。
假设标准化的加密算法要求您实现此目的:
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次。
答案 0 :(得分:24)
向C方面讲述问题,
- 我的推理是否正确,理论上这是一个合理的问题吗?
醇>
这是我以前没有考虑过的问题,但我同意你的分析。 C根据提升的左操作数的类型定义<<
运算符的行为,并且可以想象整数提升会导致(签名)int
当该操作数的原始类型为uint32_t
时。我不希望在任何现代机器上实际看到这一点,但我完全按照实际标准编程,而不是我个人的期望。
- 这个问题是否可以安全忽略,因为在每个平台上,下一个整数类型都是宽度的两倍?
醇>
C不需要整数类型之间的这种关系,尽管它在实践中无处不在。但是,如果你决心只依赖标准 - 也就是说,如果你正在努力编写严格符合规范的代码 - 那么你就不能依赖这种关系。
- 通过预先屏蔽这样的输入来正确防御这种病态是个好主意吗?:b =(a&amp; 1)&lt;&lt; 31 ;. (这在每个平台上都必须正确。但这可能 使速度关键的加密算法慢于必要。)
醇>
类型unsigned long
保证至少有32个值位,并且在整数提升下不受任何其他类型的提升。在许多常见平台上,它与uint32_t
具有完全相同的表示形式,甚至可能是同一类型。因此,我倾向于写下这样的表达式:
uint32_t a = (...);
uint32_t b = (unsigned long) a << 31;
或者,如果您只需要a
作为计算b
的中间值,那么请将其声明为unsigned long
。
答案 1 :(得分:19)
Q1:在转移之前屏蔽确实可以防止OP关注的未定义行为。
Q2:“......因为在每个平台上,下一个整数类型都是宽度的两倍?” - &GT;没有。 “下一个”整数类型可能小于2倍甚至相同大小。
以下为具有uint32_t
的所有兼容C编译器定义良好。
uint32_t a;
uint32_t b = (a & 1) << 31;
问题3:uint32_t a; uint32_t b = (a & 1) << 31;
不会产生执行掩码的代码 - 可执行文件中不需要它 - 只是在源代码中。如果确实发生了掩码,那么在速度成为问题的情况下获得更好的编译器。
作为suggested,最好用这些转变强调无符号性。
uint32_t b = (a & 1U) << 31;
@John Bollinger很好的答案详细说明了如何处理OP的具体问题。
一般问题是如何形成一个至少为n
位的数字,某个符号和不会出现令人惊讶的整数提升 - OP困境的核心。下面通过调用一个不会改变值的unsigned
操作来实现这一点 - 除了类型关注之外,有效的无操作。该产品至少 unsigned
或uint32_t
的宽度。一般来说,铸造可能会缩小类型。除非确定不会发生缩小,否则需要避免铸造。优化编译器不会创建不必要的代码。
uint32_t a;
uint32_t b = (a + 0u) << 31;
uint32_t b = (a*1u) << 31;
答案 2 :(得分:12)
从this question中了解uint32 * uint32
算术中可能的UB的线索,以下简单方法应该适用于C和C ++:
uint32_t a = (...);
uint32_t b = (uint32_t)((a + 0u) << 31);
整数常量0u
的类型为unsigned int
。这有助于将a + 0u
添加到uint32_t
或unsigned int
,以较宽者为准。由于类型的排名为int
或更高,因此不会再进行促销,并且可以应用左操作数为uint32_t
或unsigned int
的转换。
最终转回uint32_t
只会抑制有关缩小转化的潜在警告(例如int
是64位)。
一个体面的C编译器应该能够看到添加零是一个无操作,这比看到在无符号移位后预掩码无效后更加繁琐。
答案 3 :(得分:10)
为避免不必要的宣传,您可以将更大类型与某些typedef一起使用,如
using my_uint_at_least32 = std::conditional_t<(sizeof(std::uint32_t) < sizeof(unsigned)),
unsigned,
std::uint32_t>;
答案 4 :(得分:-1)
对于这段代码:
uint32_t a = (...);
uint32_t b = a << 31;
要将a
提升为无符号类型而非有符号类型,请使用:
uint32_t b = a << 31u;
当<<
运算符的两边都是无符号类型时,6.3.1.8(C标准草案n1570)中的这一行适用:
否则,如果两个操作数都具有有符号整数类型或两者都具有无符号整数类型,则具有较小整数转换等级类型的操作数将转换为具有更高等级的操作数的类型。
您所描述的问题导致您使用31
signed int type
,因此6.3.1.8中的另一行
否则,如果带有符号整数类型的操作数的类型可以表示具有无符号整数类型的操作数类型的所有值,则具有无符号整数类型的操作数将转换为带有符号整数的操作数的类型类型。
强制a
提升为签名类型
<强>更新强>
这个答案是不正确的,因为6.3.1.1(2)(强调我的):
...
如果int可以表示原始类型的所有值(限制为 按宽度,对于位字段),该值将转换为 int ; 否则,它将转换为 unsigned int 。这些被称为 整数促销.58)所有其他类型由整数保持不变 的优惠强>
和脚注58(强调我的):
58)整数提升仅适用于:通常的算术转换,某些参数表达式,一元+, - 和〜运算符的操作数,以及移位运算符的两个操作数的一部分,由各自的子条款规定。
由于只进行整数提升而不是常见的算术转换,因此使用31u
并不能保证a
如上所述转换为unsigned int
。