【问题标题】:"Overflow" assertions on various C data type: which ones are guaranteed to be true according to the C Standard?各种 C 数据类型的“溢出”断言:根据 C 标准,哪些断言是正确的?
【发布时间】:2019-09-14 19:10:07
【问题描述】:

虽然所有这些断言在我的系统上都成立,但我显然是在调用几个未定义和/或特定于实现的行为。其中一些 显然不是实际溢出。

参考this comment:这就是我问这个问题的原因。

num = num + 1 不会导致溢出。 num 自动提升为int,然后在int 中进行加法,得到128 没有溢出。然后赋值执行到char的转换。

这不是溢出,但根据 C 2018 6.3.1.3,会产生实现定义的结果或信号。这与溢出不同,因为 C 标准根本没有指定溢出时的行为,但是在这段代码中,它指定实现必须定义行为。 - 埃里克·波斯特皮希尔

我将我认为的实际行为发表评论。

因为我依赖于误解,所以我不想假设任何事情。

#include <limits.h>
#include <assert.h>
#include <stdint.h>
#include <stddef.h>

int main(void)
{
    signed char     sc = CHAR_MAX;
    unsigned char   uc = UCHAR_MAX;
    signed short    ss = SHRT_MAX;
    unsigned short  us = USHRT_MAX;
    signed int      si = INT_MAX;
    unsigned int    ui = UINT_MAX;
    signed long     sl = LONG_MAX;
    unsigned long   ul = ULONG_MAX;
    size_t          zu = SIZE_MAX;

    ++sc;
    ++uc;
    ++ss;
    ++us;
    ++si;
    ++ui;
    ++sl;
    ++ul;
    ++zu;
    assert(sc == CHAR_MIN); //integer promotion, implementation specific ?
    assert(uc == 0); //integer promotion, implementation specific ?
    assert(ss == SHRT_MIN); //integer promotion, implementation specific ? 
    assert(us == 0); //integer promotion, implementation specific ?
    assert(si == INT_MIN); //overflow & undefined
    assert(ui == 0); //wrap around: Guaranteed
    assert(sl == LONG_MIN); //overflow & undefined ?
    assert(ul == 0); //wrap around: Guaranteed ?
    assert(zu == 0); //wrap around : Guaranteed ?
    return (0);
}

【问题讨论】:

  • 我觉得num = num + 1++num可能在这方面有细微的差别
  • size_t 只是另一种类型的typedef

标签: c assert integer-overflow


【解决方案1】:

以下所有引用均来自C 2018,正式版。

int 更窄的有符号整数,二进制+

让我们首先讨论这个案例,因为它是引发这个问题的案例。考虑这段代码,它没有出现在问题中:

signed char     sc = SCHAR_MAX;
sc = sc + 1;
assert(sc == SCHAR_MIN);

6.5.6 讨论了二进制+ 运算符。第 4 段说usual arithmetic conversions 是对它们执行的。这导致sc + 1 中的sc 被转换为int1,而1 已经是int。所以sc + 1SCHAR_MAX 多出一个(通常是127 + 1 = 128),加法不存在溢出或表示问题。

然后我们必须执行分配,这将在 6.5.16.1 中讨论。第 2 段说“……右操作数的值被转换为赋值表达式的类型,并替换存储在左操作数指定的对象中的值。”所以我们必须把这个大于SCHAR_MAX的值转换成signed char,显然不能用signed char表示。

6.3.1.3 告诉我们整数的转换。对于这种情况,它说“……否则,新类型是有符号的,值不能在其中表示;结果要么是实现定义的,要么引发实现定义的信号。”

因此,我们有一个实现定义的结果或信号。这与溢出不同,溢出是在计算表达式期间结果不可表示时发生的情况。 6.5 5 表示“如果在计算表达式期间出现异常情况(即,如果结果未在数学上定义或不在其类型的可表示值范围内),则行为未定义。”例如,如果我们计算INT_MAX + 1,那么INT_MAX1的类型都是int,所以使用int类型进行运算,但是数学结果在int中是无法表示的,所以这个是一种例外情况,并且行为未由 C 标准定义。相比之下,在转换过程中,行为部分由标准定义:标准要求实现定义行为,它必须要么产生它定义的结果,要么定义一个信号。

在许多实现中,断言将评估为真。有关进一步讨论,请参阅下面的“有符号整数不小于 int”部分。

int更窄的有符号整数,前缀++

接下来,考虑这个案例,从问题中提取,除了我将CHAR_MAXCHAR_MIN更改为SCHAR_MAXSCHAR_MIN以匹配signed char类型:

signed char     sc = SCHAR_MAX;
++sc;
assert(sc == SCHAR_MIN);

我们使用一元 ++ 而不是二元 +。 6.5.3.1 2 说“前缀 ++ 的操作数的值增加了……”该子句没有明确说明执行通常的算术转换或整数提升,但它确实在第 2 段中说,“参见讨论加法运算符和复合赋值,以获取有关约束、类型、副作用和转换以及操作对指针的影响的信息。”这告诉我们它的行为类似于sc = sc + 1;,并且上面关于二进制+ 的部分适用于前缀++,因此行为是相同的。

int更窄的无符号整数,二进制+

考虑将此代码修改为使用二进制 + 而不是前缀 ++

unsigned char   uc = UCHAR_MAX;
uc = uc + 1;
assert(uc == 0);

signed char 一样,使用int 执行算术运算,然后转换为分配目标类型。这种转换由 6.3.1.3 规定:“否则,如果新类型是无符号的,则通过在新类型中可以表示的最大值的基础上重复加或减 1 来转换该值,直到该值在新类型。”因此,从数学结果 (UCHAR_MAX + 1) 中减去比最大值大一的值(也是 UCHAR_MAX + 1),直到值在范围内。单次减法得到 0,在范围内,所以结果为 0,断言为真。

int更窄的无符号整数,前缀++

考虑从问题中提取的这段代码:

unsigned char   uc = UCHAR_MAX;
++uc;
assert(uc == 0);

与前面提到的前缀++ 一样,算法与上面讨论的uc = uc + 1 相同。

有符号整数不小于int

在这段代码中:

signed int      si = INT_MAX;
++si;
assert(si == INT_MIN);

或此代码:

signed int      si = INT_MAX;
si = si + 1;
assert(si == INT_MIN);

使用int 执行算术运算。在任何一种情况下,计算都会溢出,并且行为不是由 C 标准定义的。

如果我们思考实现会做什么,有几种可能性:

  • 在二进制补码实现中,INT_MAX 加 1 产生的位模式会溢出到INT_MIN 的位模式,这就是实现有效使用的值。
  • 在一个补码实现中,将 1 加到 INT_MAX 产生的位模式会溢出到 INT_MIN 的位模式,尽管它与我们熟悉的 INT_MIN 的值不同(-2-31+1 而不是 -2-31)。
  • 在符号和幅度实现中,将 1 加到 INT_MAX 产生的位模式会溢出到 -0 的位模式。
  • 硬件检测到溢出,并出现信号。
  • 编译器在优化过程中检测到溢出并以意想不到的方式转换代码。

不小于int 的无符号整数

这种情况并不显着;该行为与上面讨论的窄于int 的情况相同:算术换行。

脚注

1 根据 Stack Overflow 其他地方的讨论,理论上 char(和 signed char)类型可能与 int 一样宽。这对 EOF 和可能的其他问题的 C 标准造成了压力,并且 C 委员会当然没有预料到。这个答案忽略了这种深奥的 C 实现,只考虑了 charint 窄的实现。

【讨论】:

  • “这个子句没有说执行通常的算术转换或整数提升。所以看起来操作是用有符号的 char 类型执行的,而不是 int 类型。”这也是我对标准的解读。然而,GCC UBSAN 确实诊断出++sc,但它确实在++si 上诊断,所以似乎不是每个人都以同样的方式阅读它......
  • @AnttiHaapala:增量/减量运算符确实执行促销活动。 6.5.2.4 ¶2 中的相关文本:“有关约束、typesconversions 以及操作对指针。” (强调我的)
  • @EricPostpischil:请注意,引用的复合赋值运算符文本是明确的:“E1 op = E2 形式的复合赋值等效于简单赋值表达式 E1 = E1 op (E2),除了……”
  • @R..:谢谢,我确定了那个解释并从答案中删除了另一个。
  • 在一个平台上,例如unsigned char 是 16 个数据位加一个填充,而int 是 16 个数据位加一个符号位,在 unsigned char 上使用 ++ 可能会溢出,标准不会强加任何要求.标准的作者可能期望任何远程健全的实现都会以环绕方式处理这种增量,而没有非常令人信服的理由不这样做,而不考虑标准是否会要求它这样做,所以没有必要担心标准是否要求这样的处理。
【解决方案2】:
assert(sc == CHAR_MIN); //integer promotion, implementation specific ?

如果 char 已签名,则取决于实现定义的 CHAR_MAX+1char 的转换;否则它是错误的,因为CHAR_MIN != SCHAR_MIN。如果CHAR_MAX==INT_MAX(可能,但不能满足托管实现的其他要求;请参阅Can sizeof(int) ever be 1 on a hosted implementation?),那么原始sc++ 是UB。

assert(uc == 0); //integer promotion, implementation specific ?

永远正确。

assert(ss == SHRT_MIN); //integer promotion, implementation specific ? 

sc 的逻辑相同。取决于实现定义的SHRT_MAX+1short 的转换,或者如果SHRT_MAX==INT_MAX 则为UB。

assert(us == 0); //integer promotion, implementation specific ?

永远正确。

assert(si == INT_MIN); //overflow & undefined

UB。

assert(ui == 0); //wrap around: Guaranteed

永远正确。

assert(sl == LONG_MIN); //overflow & undefined ?

UB。

assert(ul == 0); //wrap around: Guaranteed ?

永远正确。

assert(zu == 0); //wrap around : Guaranteed ?

永远正确。

【讨论】:

    【解决方案3】:

    各种 C 数据类型的“溢出”断言:

    符合 C 标准

    assert(uc == 0);
    assert(us == 0);
    assert(ui == 0);
    assert(ul == 0);
    assert(zu == 0);
    

    我想你想测试一下signed char sc = SCHAR_MAX; ... assert(sc == SCHAR_MIN);

    当有符号类型的范围比int更窄时:
    作为++ 重新分配的一部分,“结果是实现定义的或产生了实现定义的信号”。

    当有符号类型与int 一样宽或更宽时:
    UB 由于++ 期间有符号整数溢出。

    【讨论】:

      猜你喜欢
      • 2022-07-23
      • 2021-07-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-05-19
      • 2023-03-30
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多