首先,在谈论汇编编程时,很抱歉滥用C的术语“INT_MIN
”。但让我继续......
例如,在i386或x86_64 Linux上,C函数int neg(int x) { return -x; }
将typically translate to movl %edi, %eax; negl %eax; ret
。由于x86 neg
被定义为采用操作数的两个补码(有关详细信息,请参阅Intel64 reference manual)f
应用于INT_MIN
,其位代表为0x80000000u
返回位代表为0x7fffffffu + 1 == 0x80000000u
或INT_MIN
的数字,不会引发任何异常/陷阱。
我的问题是,是否存在任何(现代)CPU架构,其最基本的“否定”指令不一样? ARM,SPARC,MIPS,Power?嵌入式CPU?他们是否提出任何例外情况或陷入未定义的行为? (顺便说一句,我猜他们中的一些只有一个“减法”指令。)
我很好奇ntpdate(8)
中这段代码的可移植性如何:
(粗略地说,由于上述事实,它假设dostep = (NTPDATE_THRESHOLD <= abs(server->soffset));
从概念上做abs(INT_MIN) == INT_MIN
。)
答案 0 :(得分:2)
让我们看看有问题的代码(简化为最小的例子)。
bool
dostep(int32_t absoffset)
{
if (absoffset < 0)
absoffset = -absoffset;
return (absoffset >= NTPDATE_THRESHOLD || absoffset < 0);
}
很明显,作为absoffset < 0
的第二个操作数的表达式||
在没有溢出的情况下永远不会成立。但是integer overflow is undefined behavior in C。因此,完全允许编译器优化检查。
如果机器执行指令,机器如何处理整数溢出并不重要。如果使用C编程,则不编写硬件编程,则编写C标准定义的抽象机。很高兴知道真正的硬件如何推理性能。但是,根据期望编译器如何将损坏的代码转换为机器代码,对代码调用未定义的行为进行假设是很危险的。允许编译器生成任何代码,只要它使真实机器的行为符合抽象机器的标准要求即可。由于该标准明确指出 nothing 未定义的行为,因此不必对此案例进行任何假设。说穿了:NTP代码坏了。
一种可能的解决方法是像这样重新安排支票。
bool
dostep(int32_t absoffset)
{
if (absoffset < 0)
absoffset = (absoffset == INT32_MIN) ? INT32_MAX : -absoffset;
return (absoffset >= NTPDATE_THRESHOLD);
}
然而,在这种特殊情况下,存在一种更简单的解决方案。
bool
dostep(const int32_t absoffset)
{
return ((absoffset <= -NTPDATE_THRESHOLD) || (absoffset >= NTPDATE_THRESHOLD));
}
另一个选择是使用不受C标准规则限制的内联汇编,显然不可移植。
公平地说,实现可以提供它自己的C标准扩展,以便定义未定义的行为。 GCC和Clang通过提供-fwrapv
标志来实现整数溢出。来自GCC的手册页:
-fwrapv
该选项指示编译器假设加法,减法和乘法的有符号算术溢出使用二进制补码表示。此标志启用一些优化并禁用其他优化。默认情况下,Java前端根据Java语言规范的要求启用此选项。
如果NTP代码是使用GCC编译的,并且此标志和目标硬件将实现有符号整数溢出以进行环绕,则代码将是正确的。但这不再是便携式标准C。