上溢/下溢是执行时未定义的行为吗

Is over/underflow an undefined behavior at execution time?

本文关键字:未定义 下溢 执行 上溢      更新时间:2023-10-16

我读到了关于未定义行为的文章,我不确定这是一个仅限编译时的功能,还是可以在执行时发生。

我很理解这个例子(摘自维基百科的Undefined Behavior页面):

C语言的一个例子:

int foo(unsigned x)
{
int value = 5;
value += x;
if (value < 5)
bar();
return value;
}

x的值不能为负数,并且考虑到有符号整数溢出在C中是未定义的行为,编译器可以假设在if检查value >= 5的行。因此,if和对函数bar的调用可以被编译器忽略,因为if没有副作用,并且它的条件永远不会得到满足。因此,上面的代码在语义上等价于:

int foo(unsigned x)
{
int value = 5;
value += x;
return value;
}

但这发生在编译时。

如果我写,例如:

void foo(int x) {
if (x + 150 < 5)
bar();
}
int main() {
int x;
std::cin >> x;
foo(x);
}

然后用户键入MAX_INT - 100("2147483547">,如果是32位整数)。

会有一个整数溢出,但AFAIK,是CPU的算术逻辑单元会产生溢出,所以这里不涉及编译器。

它仍然是未定义的行为吗?

如果是,编译器如何检测溢出?

我能想到的最好的是CPU的溢出标志。如果是这种情况,这是否意味着如果在执行时随时设置CPU的溢出标志,编译器可以做任何他想做的事情?

是的,但不一定是我认为你的意思,也就是说,如果在机器代码中有一个加法,并且在运行时该加法包装(或者以其他方式溢出,但在大多数架构中它会包装),而不是UB本身。UB仅在C(或C++)的域中。这种添加可能是添加无符号整数,或者是编译器可以进行的某种优化,因为它知道目标平台的语义,并且可以安全地使用依赖于包装的优化(但不能,当然,除非您使用无符号类型)。

当然,这并不意味着使用"仅在运行时包装"的构造是安全的,因为这些代码路径在编译时也会受到影响。例如,在您的示例中,

extern void bar(void);
void foo(int x) {
if (x + 150 < 5)
bar();
}

由GCC 6.3编译,目标是x64到

foo:
cmp     edi, -145
jl      .L4
ret
.L4:
jmp     bar

这相当于

void foo(int x) {
if (x < -145)
bar(); // with tail call optimization
}

如果您假设有符号整数溢出是不可能的(从某种意义上说,它对输入设置了一个隐含的前提条件,使溢出不会发生),这也是一样的。

您对第一个示例的分析不正确。value += x;相当于:

value = value + x;

在这种情况下,valueintxunsigned,所以通常的算术转换意味着value首先被转换为无符号,所以我们有一个无符号加法,根据定义它不能溢出(它根据模块算术具有定义明确的语义)。

当无符号结果被分配回value时,如果它大于INT_MAX,则这是一个超出范围的分配,具有实现定义的行为。这不是溢出,因为它是赋值,而不是算术运算。

因此,哪些优化是可能的取决于实现如何定义整数的超范围赋值行为。现代系统都采用具有相同2的补码表示的值,但历史上其他系统做了一些不同的事情。

因此,原始示例在任何情况下都没有未定义的行为,并且对于大多数系统来说,建议的优化是不可能的。


第二个例子与第一个例子无关,因为它不涉及任何无符号算术。如果x > INT_MAX - 150,则表达式x + 150由于有符号整数溢出而导致未定义行为。语言定义中没有提到ALU或CPU,因此我们可以确定这些事情与行为是否未定义无关。

如果是,编译器如何检测溢出?

没有必要。确切地说,因为行为是未定义的,这意味着编译器不必担心溢出时会发生什么。它只需要发出一个可执行文件,该可执行文件举例说明所定义的情况的行为。

在这个程序中,这些是范围[INT_MININT_MAX-150]中的输入,因此编译器可以将比较转换为x < -145,因为这对定义良好的范围中的所有输入都有相同的行为,并且与未定义的情况无关。