20

考虑以下程序(C99):

#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>

int main(void)
{
    printf("Enter int in range %jd .. %jd:\n > ", INTMAX_MIN, INTMAX_MAX);
    intmax_t i;
    if (scanf("%jd", &i) == 1)
        printf("Result: |%jd| = %jd\n", i, imaxabs(i));
}

现在据我了解,这包含易于触发的未定义行为,如下所示:

Enter int in range -9223372036854775808 .. 9223372036854775807:
 > -9223372036854775808
Result: |-9223372036854775808| = -9223372036854775808

问题:

  1. 当用户输入错误的数字时,这真的是未定义的行为吗,例如“允许代码触发任何代码路径,其中任何代码都是编译器喜欢的代码”?还是其他一些未完全定义的味道?

  2. 一个迂腐的程序员如何在不做任何标准不保证的假设的情况下防止这种情况发生?

(有几个相关的问题,但我没有找到一个回答上面问题 2 的问题,所以如果你建议重复,请确保它回答了这个问题。)

4

7 回答 7

10

如果imaxabs无法表示的结果,如果使用二进制补码可能会发生,那么行为是 undefined

7.8.2.1 imaxabs 函数

  1. imaxabs 函数计算整数 j 的绝对值。如果结果无法表示,则行为未定义。221)

221) 最大负数的绝对值不能用二进制补码表示。

不做任何假设且始终定义的检查是:

intmax_t i = ... ;
if( i < -INTMAX_MAX )
{
    //handle error
}

(如果使用补码或符号幅度表示,则无法使用此 if 语句,因此编译器可能会给出无法访问的代码警告。代码本身仍然是已定义且有效的。)

于 2016-02-07T09:34:10.813 回答
7

一个迂腐的程序员如何在不做任何标准不保证的假设的情况下防止这种情况发生?

一种方法是使用无符号整数。无符号整数的溢出行为是明确定义的,从有符号整数转换为无符号整数时的行为也是如此。

所以我认为以下内容应该是安全的(事实证明它在一些非常不起眼的系统上被严重破坏,请参阅帖子后面的改进版本)

uintmax_t j = i;
if (j > (uintmax_t)INTMAX_MAX) {
  j = -j;
}
printf("Result: |%jd| = %ju\n", i, j);

那么这是如何工作的呢?

uintmax_t j = i;

这会将有符号整数转换为无符号整数。如果为正,则值保持不变,如果为负,则值增加 2 n(其中 n 是位数)。这会将其转换为一个大数(大于 INTMAX_MAX)

if (j > (uintmax_t)INTMAX_MAX) {

如果原始数字是正数(因此小于或等于 INTMAX_MAX),则什么也不做。如果原始数字为负,则运行 if 块的内部。

  j = -j;

数被否定。否定的结果显然是负数,因此不能表示为无符号整数。所以它增加了 2 n

所以在代数上,负 i 的结果看起来像

j = - (i + 2 n ) + 2 n = -i


聪明,但这个解决方案做了假设。如果 C 标准允许的 INTMAX_MAX == UINTMAX_MAX,则会失败。

嗯,让我们看看这个(我正在阅读https://busybox.net/~landley/c99-draft.html,这显然是标准化之前的最后一个 C99 草案,如果最终标准有任何变化,请告诉我。

当 typedef 名称仅在初始 u 不存在或存在时有所不同时,它们应表示相应的有符号和无符号类型,如 6.2.5 所述;一个实现不应该提供一个类型而不提供其对应的类型。

在 6.2.5 我看到

对于每个有符号整数类型,都有一个对应的(但不同的)无符号整数类型(用关键字 unsigned 指定),它使用相同的存储量(包括符号信息)并具有相同的对齐要求。

在 6.2.6.2 我看到

#1

对于 unsigned char 以外的无符号整数类型,对象表示的位应分为两组:值位和填充位(后者不需要任何一个)。如果有 N 个值位,每个位应表示 1 和 2N-1 之间的 2 的不同幂,以便 > 该类型的对象应能够表示从 0 到 2N-1 的值 > 使用纯二进制表示;这应称为值表示。任何填充位的值都未指定。39)

#2

对于有符号整数类型,对象表示的位应分为三组:值位、填充位和符号位。不需要任何填充位;应该只有一个符号位。作为值位的每个位应与相应无符号类型的对象表示中的相同位具有相同的值(如果有符号类型中有 M 个值位,无符号类型中有 N 个值位,则 M<=N)。如果符号位为零,则不应影响结果值。

所以是的,看起来你是对的,虽然有符号和无符号类型必须具有相同的大小,但对于无符号类型来说,它似乎确实比有符号类型多一个填充位是有效的。


好的,基于上面的分析揭示了我第一次尝试中的一个缺陷,我编写了一个更加偏执的变体。这与我的第一个版本相比有两个变化。

我使用 i < 0 而不是 j > (uintmax_t)INTMAX_MAX 来检查负数。这意味着即使 INTMAX_MAX == UINTMAX_MAX,算法也会对大于或等于 -INTMAX_MAX 的数字产生正确的结果。

我为 INTMAX_MAX == UINTMAX_MAX、INTMAX_MIN == -INTMAX_MAX -1 和 i == INTMAX_MIN 的错误情况添加了处理。这将导致我们可以轻松测试的 if 条件内的 j=0。

从C标准的要求可以看出,INTMAX_MIN不能小于-INTMAX_MAX -1,因为符号位只有一个,值位的个数必须等于或小于对应的无符号类型。根本没有剩下的位模式来表示较小的数字。

uintmax_t j = i;
if (i < 0) {
  j = -j;
  if (j == 0) {
    printf("your platform sucks\n");
    exit(1);
  }
}
printf("Result: |%jd| = %ju\n", i, j);

@plugwash 我认为 2501 是正确的。例如,-UINTMAX_MAX 值变为 1:(-UINTMAX_MAX + (UINTMAX_MAX + 1)),并且不会被您的 if 捕获。– 海德 58 分钟前

嗯,

假设 INTMAX_MAX == UINTMAX_MAX 和 i = -INTMAX_MAX

uintmax_t j = i;

在此命令之后 j = -INTMAX_MAX + (UINTMAX_MAX + 1) = 1

如果 (i < 0) {

i 小于零,所以我们在 if 中运行命令

j = -j;

在此命令之后 j = -1 + (UINTMAX_MAX + 1) = UINTMAX_MAX

这是正确的答案,因此无需在错误情况下将其捕获。

于 2016-02-07T11:29:22.853 回答
4

在二补系统上,获得最大负值的绝对数确实是未定义的行为,因为绝对值会超出范围。由于 UB 在运行时发生,因此编译器无法帮助您。

防止这种情况的唯一方法是将输入与类型的最负值进行比较(INTMAX_MIN在您显示的代码中)。

于 2016-02-07T08:52:32.527 回答
2

因此,计算整数的绝对值会在一种情况下调用未定义的行为。实际上,虽然可以避免未定义的行为,但在一种情况下不可能给出正确的结果。

现在考虑一个整数乘以 3: 这里有一个更严重的问题。此操作在所有情况下的 2/3 中调用了未定义的行为!对于三分之二的 int 值 x,要找到一个值为 3x 的 int 是不可能的。这是一个比绝对值问题严重得多的问题。

于 2016-02-07T22:38:14.853 回答
1

您可能想使用一些技巧:

int v;           // we want to find the absolute value of v
unsigned int r;  // the result goes here 
int const mask = v >> sizeof(int) * CHAR_BIT - 1;

r = (v + mask) ^ mask;

这在INT_MIN < v <= INT_MAX. 在这种情况下v == INT_MIN,它仍然存在INT_MIN而不会导致未定义的行为

您还可以使用按位运算在一个的补码和符号幅度系统上处理这个问题。

参考:https ://graphics.stanford.edu/~seander/bithacks.html#IntegerAbs

于 2016-02-07T09:51:32.527 回答
0

根据这个http://linux.die.net/man/3/imaxabs

笔记

试图取最大负整数的绝对值是没有定义的。

要处理全部范围,您可以在代码中添加类似这样的内容

    if (i != INTMAX_MIN) {
        printf("Result: |%jd| = %jd\n", i, imaxabs(i));
    } else {  /* Code around undefined abs( INTMAX_MIN) /*
        printf("Result: |%jd| = %jd%jd\n", i, -(i/10), -(i%10));
    }

编辑:由于 abs(INTMAX_MIN) 不能在 2 的补码机器上表示,因此可表示范围内的 2 个值在输出时连接为字符串。用 gcc 测试,虽然 printf 需要 %lld 因为 %jd 不是受支持的格式。

于 2016-02-07T09:34:57.967 回答
-1
  1. 当用户输入错误的数字时,这真的是未定义的行为吗,例如“允许代码触发任何代码路径,其中任何代码都是编译器喜欢的代码”?还是其他一些未完全定义的味道?

只有当错误的数字被成功输入并传递给 imaxabs() 时,程序的行为才被定义,这在典型的 2 的补码系统上返回 -ve 结果,如您所见。

在这种情况下,这是未定义的行为,如果 ALU 设置状态标志,则还允许实现以溢出错误终止程序。

C 中“未定义行为”的原因是编译器编写者不必防范溢出,因此程序可以更有效地运行。虽然每个使用 abs() 试图杀死你的第一个出生的 C 程序都符合 C 标准,但仅仅因为你用一个太 -ve 的值调用它,将这样的代码写入目标文件将是不正当的。

这些未定义行为的真正问题是优化编译器可以推理出天真的检查,因此代码如下:

r = (i < 0) ? -i : i;
if (r < 0) {   // This code may be pointless
    // Do overflow recovery
    doRecoveryProcessing();
} else {
    printf("%jd", r);
}

由于编译器优化器可以推断负值被否定,它原则上可以确定 (r <0)始终为假,因此捕获问题的尝试失败。

  1. 一个迂腐的程序员如何在不做任何标准不保证的假设的情况下防止这种情况发生?

到目前为止,最好的方法是确保程序在有效范围内工作,因此在这种情况下验证输入就足够了(不允许 INTMAX_MIN)。打印 abs() 表格的程序应该避免使用 INT*_MIN 等。

    if (i != INTMAX_MIN) {
        printf("Result: |%jd| = %jd\n", i, imaxabs(i));
    } else {  /* Code around undefined abs( INTMAX_MIN) /*
        printf("Result: |%jd| = %jd%jd\n", i, -(i/10), -(i%10));
    }

似乎是通过伪造写出 abs(INTMAX_MIN),从而使程序能够兑现对用户的承诺。

于 2016-02-07T18:17:26.440 回答