有符号整数溢出是未定义的行为还是已定义的实现?

问题描述

#include <limits.h>

int main(){
 int a = UINT_MAX; 
 return 0;
}

我定义了这个 UB 或实现吗?

链接说它的 UB

https://www.gnu.org/software/autoconf/manual/autoconf-2.63/html_node/Integer-Overflow-Basics

Allowing signed integer overflows in C/C++

链接说明其实现定义

http://www.enseignement.polytechnique.fr/informatique/INF478/docs/Cpp/en/c/language/signed_and_unsigned_integers.html

转换规则说:

否则,新类型是有符号的,值不能在其中表示;要么结果是实现定义的,要么引发实现定义的信号。

我们不是将 max unsigned value 转换为 signed value 吗?

在我看来,gcc 只是截断了结果。

解决方法

两个参考文献都是正确的,但它们没有解决相同的问题。

int a = UINT_MAX; 不是有符号整数溢出的实例,此定义涉及从 unsigned intint 的转换,其值超出类型 int 的范围。正如École polytechnique 的网站所引用的那样,C 标准将行为定义为实现定义。

#include <limits.h>

int main(){
    int a = UINT_MAX;    // implementation defined behavior
    int b = INT_MAX + 1; // undefined behavior
    return 0;
}

以下是来自 C 标准的文本:

6.3.1.3 有符号和无符号整数

  1. 当一个整数类型的值转换为除_Bool以外的其他整数类型时,如果该值可以用新类型表示,则不变。

  2. 否则,如果新类型是无符号的,则通过重复加或减一个新类型可以表示的最大值来转换该值,直到该值在新类型的范围内。

  3. 否则,新类型是有符号的,值不能在其中表示;要么结果是实现定义的,要么引发实现定义的信号。

一些编译器有一个命令行选项来将有符号算术溢出的行为从未定义的行为更改为实现定义的行为:gccclang 支持 -fwrapv 以强制执行整数计算根据签名类型对 232 或 264 取模。这阻止了一些有用的优化,但也阻止了一些可能破坏无辜代码的违反直觉的优化。有关一些示例,请参阅此问题:What does -fwrapv do?

,

这是无符号整数溢出:

int a = UINT_MAX; 

它是从无符号到有符号整数类型的转换,并且是实现定义C standard 的第 6.3.1.3 节关于有符号和无符号整数类型的转换对此进行了介绍:

1 当一个整数类型的值被转换为除_Bool之外的另一个整数类型时,如果该值可以用新的 类型,不变。

2 否则,如果新类型是无符号的,则通过重复加减一比来转换值 可以在新类型中表示的最大值 直到值在新类型的范围内。6

3 否则,新类型是有符号的,值不能在其中表示;结果要么是实现定义的 或者引发了实现定义的信号。

有符号整数溢出的一个例子是:

int x = INT_MAX;
x = x + 1;

而且这个未定义的。事实上,C 标准的第 3.4.3 节在第 4 段中定义了未定义的行为状态:

未定义行为的一个例子是整数溢出行为

整数溢出仅适用于 6.2.5p9 中的有符号类型:

有符号整数类型的非负值范围是对应的无符号整数类型的子范围,每个类型中相同值的表示是相同的。 涉及无符号操作数的计算永远不会溢出,因为无法由结果无符号整数类型表示的结果以比结果类型可以表示的最大值大一的数为模减少

,

int a = UINT_MAX; 不会溢出,因为在评估此声明或其中的表达式时没有发生异常情况。此代码定义UINT_MAX转换为int类型用于a的初始化,转换由C 2018 6.3.1.3中的规则定义.

简而言之,适用的规则是:

  • 6.7.9 11 说初始化的行为类似于简单的赋值:“……对象的初始值是表达式的初始值(转换后);应用与简单赋值相同的类型约束和转换,...​​”
  • 6.5.16.1 2 说简单赋值执行转换:“在简单赋值=)中,右操作数的值被转换为赋值表达式的类型并替换存储在左操作数指定的对象中的值。”
  • 6.3.1.3 3 涵盖了当操作数值无法在类型中表示时转换为有符号整数类型的内容:“结果要么是实现定义的,要么引发了实现定义的信号。”

因此,行为已定义。

在 2018 6.5 5 中有一个关于计算表达式时发生的异常情况的一般规则:

如果在表达式求值过程中出现异常情况(即,如果结果未在数学上定义或不在其类型的可表示值范围内),则行为未定义。

然而,这条规则从不适用于上面的链。在进行评估时,包括初始化的隐含赋值,我们永远不会得到超出其类型范围的结果。转换的输入超出目标类型int的范围,但转换的结果在范围内,因此没有超出范围超出范围的结果会触发异常情况。

(一个可能的例外是,我想 C 实现可以将转换的结果定义为超出 int 的范围。我不知道任何这样做,这很可能不是 6.3.1.3 3 的意图。)

,

在预先存在的“语言”(方言系列)中,C 标准是用来描述的,实现通常要么通过做底层平台所做的任何事情来处理有符号整数溢出,将值截断到底层类型的长度(这是大多数平台所做的),即使是在否则会做其他事情或触发某种形式的信号或诊断的平台上。 在 K&R 的《The C Programming Language》一书中,这种行为被描述为“依赖机器”。

尽管标准的作者在已发布的基本原理文档中指出了一些他们希望普通平台的实现以普通方式运行的情况,但他们不想说某些操作会在某些平台上定义行为但不是其他人。此外,将行为描述为“实现定义”会产生问题。考虑如下:

int f1(void);
int f2(int a,int b,int c);

int test(int x,int y)
{
  int test = x*y;
  if (f1())
    f2(test,x,y);
}

如果整数溢出的行为是“实现定义的”,那么任何可能引发信号或具有其他可观察副作用的实现都需要在调用 f1() 之前执行乘法,即使乘法的结果是除非 f1() 返回非零值,否则将被忽略。将其归类为“未定义行为”可避免此类问题。

不幸的是,gcc 将分类解释为“未定义行为”,以邀请以不受普通因果关系定律约束的方式处理整数溢出。给定一个函数,如:

unsigned mul_mod_32768(unsigned short x,unsigned short y)
{
  return (x*y) & 0x7FFFu;
}

尝试使用大于 xINT_MAX/y 调用它可能会任意破坏周围代码的行为,即使函数的结果不会以任何可观察的方式使用。

>