位移位超出允许范围
bitshift outside allowed range
我们在生产中有一个代码,在某些情况下可能会将 32 位无符号整数左移超过 31 位。
我知道这被认为是未定义的行为。不幸的是,我们现在无法解决这个问题,但我们可以解决这个问题,只要我们可以假设它在实践中是如何工作的。
On x86/amd64 我知道移位处理器只使用移位计数操作数的适当的次要位。所以 a << b
实际上等同于 a << (b & 31)
。从硬件设计来看,这是完全合理的。
我的问题是:这在现代流行平台(如 arm、mips、RISC 等)上如何实际工作。我的意思是那些实际用于现代 PC 和移动设备的平台,既不过时也不深奥。
我们可以假设它们的行为方式相同吗?
编辑:
我所说的代码目前在区块链中运行。它究竟如何工作并不重要,但至少我们要确保它在所有机器上产生 相同 结果。这是最重要的,否则可以利用它来引发所谓的链分裂。
修复这个问题很麻烦,因为修复应该同时应用于所有 运行 机器,否则我们将再次面临链分裂的风险。但我们会在某个时候以有组织(受控)的方式进行。
不同编译器的问题较少。我们只使用 GCC。我亲眼看了代码,那里有一条 shl
指令。坦率地说,我不希望它在上下文中有任何不同(移位操作数来自任意来源,无法在编译时预测)。
请不要提醒我“不能假设”。我知道这个。我的问题是 100% 实用的。正如我所说,我 知道 在 x86/amd64 上,32 位移位指令仅占用位计数操作数的 5 个最低有效位。
这在当前的现代架构上表现如何?我们也可以将问题限制在小端处理器上。
对于触发 未定义行为 的代码,编译器几乎可以做任何事情 - 好吧,这就是它未定义的原因 - 要求对未定义代码进行安全定义不会产生任何影响感觉。理论评估或观察编译器翻译类似代码或假设“常见做法”可能不会真正给你答案。
评估编译器真正翻译您的 UB 代码的内容可能是您唯一安全的选择。如果您想真正确定极端情况下会发生什么,请查看生成的(汇编或机器)代码。现代调试器为您提供了工具集来捕获这些极端情况并告诉您实际发生了什么(生成的机器代码毕竟定义得很好)。这比推测编译器可能发出的代码要简单得多,也安全得多。
它是C中的UB。这里有一个例子:https://godbolt.org/z/5h9f7W6rr
它没有任何实际用途,除非你使用内联汇编。但是如果你想了解特定平台如何处理左移汇编指令,你需要查看它们的汇编语言参考:
ARM-拇指
If n is 32 or more, then all the bits in the result are cleared to 0.
If n is 33 or more and the carry flag is updated, it is updated to 0.
x86 - 以 32 为模执行移位。
该标准的作者试图将其行为可能不容易以与 C 抽象模型和目标平台上操作的一般原则一致的方式一致地描述的任何行为归类为未定义行为,或者其行为可能明显受到优化的影响。尽管几乎所有具有 N 移位指令的处理器都会通过计算 (x<<(y-1)<<1) 或 (x<<( y-bitsize)) 没有副作用,“没有副作用”这个短语有时可能有点模糊。在某些处理器(例如 IIRC,至少一种 Transputer 型号)上,当 (uint32_t)y 为 0xFFFFFFFF 时直接处理 x<<y
会导致处理器花费数十亿个周期来执行一条指令, 无法处理中断。虽然执行指令的时间通常不会被视为副作用,但忽略中断达数十亿个周期可能会破坏系统稳定性。
对于其移位指令始终以所描述的两种方式之一运行的体系结构,除了以其中一种方式处理代码之外,没有任何理由邀请通用编译器执行任何操作,但标准没有区别在针对不寻常平台或寻求标记代码可能与它们不兼容的地方的实现的行为让步与邀请普通架构的通用编译器将行为先例抛出 window.
之间
如果在没有令人信服的理由的情况下使用旨在维护长期存在的行为先例的实现,则不考虑标准是否需要此类行为,x<
我们在生产中有一个代码,在某些情况下可能会将 32 位无符号整数左移超过 31 位。 我知道这被认为是未定义的行为。不幸的是,我们现在无法解决这个问题,但我们可以解决这个问题,只要我们可以假设它在实践中是如何工作的。
On x86/amd64 我知道移位处理器只使用移位计数操作数的适当的次要位。所以 a << b
实际上等同于 a << (b & 31)
。从硬件设计来看,这是完全合理的。
我的问题是:这在现代流行平台(如 arm、mips、RISC 等)上如何实际工作。我的意思是那些实际用于现代 PC 和移动设备的平台,既不过时也不深奥。
我们可以假设它们的行为方式相同吗?
编辑:
我所说的代码目前在区块链中运行。它究竟如何工作并不重要,但至少我们要确保它在所有机器上产生 相同 结果。这是最重要的,否则可以利用它来引发所谓的链分裂。
修复这个问题很麻烦,因为修复应该同时应用于所有 运行 机器,否则我们将再次面临链分裂的风险。但我们会在某个时候以有组织(受控)的方式进行。
不同编译器的问题较少。我们只使用 GCC。我亲眼看了代码,那里有一条
shl
指令。坦率地说,我不希望它在上下文中有任何不同(移位操作数来自任意来源,无法在编译时预测)。请不要提醒我“不能假设”。我知道这个。我的问题是 100% 实用的。正如我所说,我 知道 在 x86/amd64 上,32 位移位指令仅占用位计数操作数的 5 个最低有效位。
这在当前的现代架构上表现如何?我们也可以将问题限制在小端处理器上。
对于触发 未定义行为 的代码,编译器几乎可以做任何事情 - 好吧,这就是它未定义的原因 - 要求对未定义代码进行安全定义不会产生任何影响感觉。理论评估或观察编译器翻译类似代码或假设“常见做法”可能不会真正给你答案。
评估编译器真正翻译您的 UB 代码的内容可能是您唯一安全的选择。如果您想真正确定极端情况下会发生什么,请查看生成的(汇编或机器)代码。现代调试器为您提供了工具集来捕获这些极端情况并告诉您实际发生了什么(生成的机器代码毕竟定义得很好)。这比推测编译器可能发出的代码要简单得多,也安全得多。
它是C中的UB。这里有一个例子:https://godbolt.org/z/5h9f7W6rr
它没有任何实际用途,除非你使用内联汇编。但是如果你想了解特定平台如何处理左移汇编指令,你需要查看它们的汇编语言参考:
ARM-拇指
If n is 32 or more, then all the bits in the result are cleared to 0.
If n is 33 or more and the carry flag is updated, it is updated to 0.
x86 - 以 32 为模执行移位。
该标准的作者试图将其行为可能不容易以与 C 抽象模型和目标平台上操作的一般原则一致的方式一致地描述的任何行为归类为未定义行为,或者其行为可能明显受到优化的影响。尽管几乎所有具有 N 移位指令的处理器都会通过计算 (x<<(y-1)<<1) 或 (x<<( y-bitsize)) 没有副作用,“没有副作用”这个短语有时可能有点模糊。在某些处理器(例如 IIRC,至少一种 Transputer 型号)上,当 (uint32_t)y 为 0xFFFFFFFF 时直接处理 x<<y
会导致处理器花费数十亿个周期来执行一条指令, 无法处理中断。虽然执行指令的时间通常不会被视为副作用,但忽略中断达数十亿个周期可能会破坏系统稳定性。
对于其移位指令始终以所描述的两种方式之一运行的体系结构,除了以其中一种方式处理代码之外,没有任何理由邀请通用编译器执行任何操作,但标准没有区别在针对不寻常平台或寻求标记代码可能与它们不兼容的地方的实现的行为让步与邀请普通架构的通用编译器将行为先例抛出 window.
之间如果在没有令人信服的理由的情况下使用旨在维护长期存在的行为先例的实现,则不考虑标准是否需要此类行为,x<