C安全地取整数

时间:2016-02-07 08:41:39

标签: c undefined-behavior absolute-value

考虑以下程序(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. 一个迂腐的程序员如何防止这种情况,没有做出标准无法保证的任何假设?

  3. (有一些相关问题,但我没有找到一个回答上面问题2的问题,所以如果你建议复制,请确保答案。)

7 个答案:

答案 0 :(得分:10)

如果imaxabs的结果无法表示,如果使用两个补码就会发生,那么行为是未定义的

  

7.8.2.1 imaxabs功能

     
      
  1. imaxabs函数计算整数j的绝对值。如果结果不能   表示,行为是不确定的。 221)
  2.         

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

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

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

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

答案 1 :(得分: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

所以代数上负面的结果我看起来像

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

  

聪明,但这个解决方案做出了假设。如果IC标准允许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&lt; 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捕获。 - hyde 58分钟前

嗯,

假设INTMAX_MAX == UINTMAX_MAX且i = -INTMAX_MAX

uintmax_t j = i;

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

if(i <0){

我小于零所以我们在if

中运行命令

j = -j;

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

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

答案 2 :(得分:4)

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

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

答案 3 :(得分:2)

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

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

答案 4 :(得分: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

答案 5 :(得分: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不是支持的格式。

答案 6 :(得分:-1)

  1. 这是否真的是未定义的行为,因为在&#34;代码被允许触发任何代码路径,当用户输入错误的数字时,任何代码都会使编译器感到奇怪?#34;还是其他一些未完全定义的风格?
  2. 程序的行为只是未定义,当错误的数字被成功输入并传递给imaxabs()时,在典型的2补码系统中会返回-ve结果。

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

    &#34;未定义行为的原因&#34;在C中,编译器编写者不必防止溢出,因此程序可以更有效地运行。虽然每个使用abs()来尝试杀死你的第一个生命的C程序都在C标准范围内,只是因为你用太高的值来调用它,把这样的代码写入目标文件只会是不正常的。

    这些未定义行为的真正问题在于,优化编译器可以推断出天真的检查,所以代码如下:

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

    作为编译器,optomiser可以推断负值被否定,它原则上可以确定(r <0)总是为false,因此陷阱问题的尝试失败。

    1. 一个迂腐的程序员如何在没有做出标准无法保证的假设的情况下对此进行防范?
    2. 到目前为止,最好的方法是确保程序在有效范围内工作,因此在这种情况下验证输入就足够了(不允许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),允许程序实现它对用户的承诺。