【问题标题】:Don't understand "assuming signed overflow" warning不理解“假设签名溢出”警告
【发布时间】:2012-10-20 02:58:15
【问题描述】:

我得到:

警告:假设 (X + c)

在这一行:

if ( this->m_PositionIndex[in] < this->m_EndIndex[in] ) 

m_PositionIndexm_EndIndex 类型为 itk::Index (http://www.itk.org/Doxygen/html/classitk_1_1Index.html),它们的 operator[] 返回一个 signed long

(这里是第 37 行:https://github.com/Kitware/ITK/blob/master/Modules/Core/Common/include/itkImageRegionConstIteratorWithIndex.hxx 用于上下文)

任何人都可以在这里解释导致该警告的原因吗?我在任何地方都看不到 (x+c) &lt; x 模式 - 因为这只是 signed long 比较。

我试图在一个独立的例子中重现它:

#include <iostream>

namespace itk
{
  struct Index
  {
    signed long data[2];

    Index()
    {
      data[0] = 0;
      data[1] = 0;
    }

    signed long& operator[](unsigned int i)
    {
      return data[i];
    }
  };
}
int main (int argc, char *argv[])
{
  itk::Index positionIndex;
  itk::Index endIndex;

  for(unsigned int i = 0; i < 2; i++)
  {
    positionIndex[i]++;
    if ( positionIndex[i] < endIndex[i] )
    {
      std::cout << "something" << std::endl;
    }
  }

  return 0;
}

但我没有收到警告。关于我的演示和真实代码之间有什么不同的想法,或者是什么可能导致真实代码中的警告?我收到带有 -Wall 标志的 gcc 4.7.0 和 4.7.2 的警告。

【问题讨论】:

  • 当您编译您的演示时,您是否使用了相同的优化级别? -fstrict-overflow-O2(或更高版本)
  • @DaveS - 是的,我在两种情况下都使用 -O3 -Wall 进行编译。
  • 看看使用这个operator++的“实例化”函数的反汇编可能会很有趣。

标签: c++ gcc gcc-warning


【解决方案1】:

要简单地禁用此警告,请使用 -Wno-strict-overflow 要禁用触发此警告的特定优化,请使用 -fno-strict-overflow-fwrapv

gcc manpage 描述了这个警告可以用级别来控制:-Wstrict-overflow=n

如果由于-Werror 而导致您的构建停止,您可以使用-Wno-error=strict-overflow(或仅使用-Wno-error 覆盖-Werror)来解决而不隐藏警告。


分析和评论...

我收到了同样的警告,并花了几个小时试图在一个较小的示例中重现它,但从未成功。我的真实代码涉及在模板类中调用内联函数,但算法简化为以下...

int X = some_unpredictable_value_well_within_the_range_of_int();
for ( int c=0; c<4; c++ ) assert( X+c >= X ); ## true unless (X+c) overflows

在我的例子中,警告与展开for 循环的优化器有某种关联,因此我可以通过声明volatile int c=0 来解决问题。修复它的另一件事是声明unsigned int c=0,但我不确定为什么会有所作为。修复它的另一件事是使循环计数足够大以至于循环不会被展开,但这不是一个有用的解决方案。

那么这个警告的真正含义是什么?要么是说优化器修改了你的算法的语义(假设没有溢出),要么只是告诉你优化器假设你的代码没有未定义的行为 em> 溢出有符号整数。除非溢出有符号整数是您的程序预期行为的一部分,否则此消息可能并不表示您的代码存在问题——因此您可能希望在正常构建时禁用它。如果您收到此警告但不确定有问题的代码,则使用-fwrapv 禁用优化可能是最安全的。

顺便说一句,我在 GCC 4.7 上遇到了这个问题,但是使用 4.8 编译的相同代码没有警告——这可能表明 GCC 开发人员认识到在正常情况下需要不那么“严格”(或也许这只是由于优化器的差异)。


哲学...

在 [C++11:N3242$5.0.4] 中,它声明...

如果在计算表达式期间,结果不是 数学定义或不在可表示值的范围内 它的类型,行为未定义。

这意味着符合标准的编译器可以简单地假设不会发生溢出(即使对于无符号类型)。

在 C++ 中,由于模板和复制构造函数等特性,elision 的无意义操作是重要的优化器能力。但是,有时,如果您将 C++ 用作低级“系统语言”,您可能只是希望编译器按照您的要求执行操作——并依赖于底层硬件的行为。鉴于标准的语言,我不确定如何以独立于编译器的方式实现这一点。

【讨论】:

  • 本质上,程序告诉你有一个不必要的条件。在您的情况下,编译器可以推断出 c 始终为非负数,因此您的断言根据语言规则是正确的,因此是不必要的。如果您想检测溢出,请在语言规则内进行 (INT_MAX - c &gt;= X)。
  • @SebastianRedl:assuming signed overflow does not occur when assuming that (X + c) &lt; X is always false。警告不是关于条件是不必要的事实 - 它是关于为了优化条件而做出的假设。关于溢出检查的好建议,但我认为我的方式更清晰(并且可以,因为它只是一个评论)。
  • 我的示例代码看起来有点傻,但那是因为我简化了它。在实际代码中,断言的措辞不同,并隐藏在函数调用中。在一般情况下,它是一个有意义的断言,但在这种特殊情况下,优化器足够聪明,可以发现它可以优化它。不幸的是,这导致了一条毫无意义的警告信息,迫使我进行调查。
  • 是的,真正的问题是 GCC 在内联后发出了这个警告,这导致它可以推断在这个特定的实例中,条件总是错误的。出现警告的原因是开发人员以这种方式测试溢出而导致严重的安全错误,而 GCC 只是因为代码未定义而优化了检查。
  • '只是禁用警告' 在 C 中,这是不可接受的 -1
【解决方案2】:

编译器告诉你它有足够的关于 sn-p 的静态知识,知道如果它可以优化测试,假设没有签名操作会溢出,那么测试总是会成功。

换句话说,当 x 被签名时,x + 1 &lt; x 将返回 true 的唯一方法是 x 已经是最大的签名值。 [-Wstrict-overflow] 让编译器在假设没有符号加法溢出时发出警告;它基本上是在告诉您它将优化测试,因为签名溢出是未定义的行为。

如果你想抑制警告,摆脱测试。

【讨论】:

    【解决方案3】:

    尽管您的问题已经很老了,但由于您还没有更改代码的那部分,我认为问题仍然存在并且您仍然没有得到关于这里实际发生的事情的有用解释,所以让我的试试看:

    在 C(和 C++)中,如果添加两个 SIGNED 整数导致溢出,则整个程序运行的行为是 UNDEFINED。因此,执行您的程序的环境可以为所欲为(格式化您的硬盘或发动一场核战争,假设存在必要的硬件)。

    gcc 通常两者都不做,但它会做其他令人讨厌的事情(在不幸的情况下仍可能导致其中任何一个)。为了证明这一点,让我举一个来自 Felix von Leitner @http://ptrace.fefe.de/int.c 的简单例子:

    #include <assert.h>
    #include <stdio.h>
    
    int foo(int a) {
      assert(a+100 > a);
      printf("%d %d\n",a+100,a);
      return a;
    }
    
    int main() {
      foo(100);
      foo(0x7fffffff);
    }
    

    注意:我添加了 stdio.h,以消除有关未声明 printf 的警告。

    现在,如果我们运行它,我们希望代码在第二次调用 foo 时断言,因为它会创建一个整数溢出并检查它。所以,让我们开始吧:

    $ gcc -O3 int.c -o int && ./int
    200 100
    -2147483549 2147483647
    

    WTF? (WTF,德语为“Was täte Fefe”-“Fefe 会做什么”,Fefe 是 Felix von Leitner 的昵称,我借用了代码示例)。哦,顺便说一句,是 täte Fefe 吗?正确:在 2007 年就这个问题写一个错误报告! https://gcc.gnu.org/bugzilla/show_bug.cgi?id=30475

    回到你的问题。如果您现在尝试深入挖掘,您可以创建一个程序集输出 (-S) 并进行调查以发现 assert 已完全删除

    $ gcc -S -O3 int.c -o int.s && cat int.s
    [...]
    foo:
            pushq   %rbx             // save "callee-save" register %rbx
            leal    100(%rdi), %edx  // calc a+100 -> %rdx for printf
            leaq    .LC0(%rip), %rsi // "%d %d\n" for printf
            movl    %edi, %ebx       // save `a` to %rbx
            movl    %edi, %ecx       // move `a` to %rcx for printf
            xorl    %eax, %eax       // more prep for printf
            movl    $1, %edi         // and even more prep
            call    __printf_chk@PLT // printf call
            movl    %ebx, %eax       // restore `a` to %rax as return value
            popq    %rbx             // recover "callee-save" %rbx
            ret                      // and return
    

    这里任何地方都没有断言。

    现在,让我们在编译期间打开警告。

    $ gcc -Wall -O3 int.c -o int.s
    int.c: In function 'foo':
    int.c:5:2: warning: assuming signed overflow does not occur when assuming that (X + c) >= X is always true [-Wstrict-overflow]
      assert(a+100 > a);
      ^~~~~~
    

    所以,这条消息实际上说的是:a + 100 可能会溢出导致未定义的行为。因为你是一个技术娴熟的专业软件开发人员,从不做错任何事,我 (gcc)知道肯定,a + 100 不会溢出。因为我知道,我也知道,a + 100 &gt; a 总是正确的。因为我知道,我知道断言永远不会触发。因为我知道这一点,所以我可以在“死代码消除”优化中消除整个断言。

    这正是 gcc 在这里所做的(并警告您)。

    现在,在你的小例子中,数据流分析可以确定,这个整数实际上没有溢出。所以,gcc 不需要假设它永远不会溢出,相反,gcc 可以证明它永远不会溢出。在这种情况下,删除代码是绝对可以的(编译器仍然可以在此处警告死代码消除,但 dce 经常发生,可能没有人想要这些警告)。但是在你的“真实世界代码”中,数据流分析失败了,因为并非所有必要的信息都存在。所以,gcc 不知道a++ 是否溢出。所以它警告你,它假设永远不会发生(然后 gcc 删除整个 if 语句)。

    解决(或隐藏!!!)这里问题的一种方法是在执行a++ 之前为a &lt; INT_MAX 断言。无论如何,我担心你可能有一些真正的错误,但我必须进行更多调查才能弄清楚。但是,您可以自己弄清楚,以正确的方式创建您的 MVCE:获取带有警告的源代码,从包含文件中添加任何必要的内容以获取独立的源代码(gcc -E 有点极端,但会做这项工作),然后开始删除任何不会使警告消失的东西,直到你有一个你不能再删除任何东西的代码。

    【讨论】:

      【解决方案4】:

      就我而言,这是来自编译器的一个很好的警告。我有一个愚蠢的复制/粘贴错误,我在循环中有一个循环,每个循环都是相同的条件集。像这样的:

      for (index = 0; index < someLoopLimit; index++)
      {
          // blah, blah, blah.....
          for (index = 0; index < someLoopLimit; index++)
          {
              //  More blah, blah, blah....
          }
      }
      

      这个警告为我节省了调试时间。谢谢,gcc!!!

      【讨论】:

        【解决方案5】:

        这几乎可以肯定是一个错误,尽管我不知道它是三个可能的错误中的哪一个。

        我相信,gcc 以某种方式计算出 'this->m_PositionIndex[in]' 等中的值是从什么计算得出的,即两个值都来自同一个值,一个带有偏移量的值它也可以证明要积极。因此,它表明 if/else 构造的一个分支是无法访问的代码。

        这很糟糕。确实。为什么?嗯,正好有三种可能:

        1. gcc 是正确的,因为代码无法访问,而您忽略了这种情况。在这种情况下,您只是拥有臃肿的代码。三种可能性中最好的,但在代码质量方面仍然很差。

        2. gcc 是正确的,因为代码无法访问,但您希望它可以访问。在这种情况下,您需要再次查看它无法访问的原因并修复您的逻辑中的错误。

        3. gcc 错误,您偶然发现了其中的一个错误。不太可能,但有可能,不幸的是很难证明。

        真正糟糕的是,您绝对需要找出代码无法访问的确切原因,或者您需要证明 gcc 是正确的(说起来容易做起来难)。根据墨菲定律,假设情况 1. 或 3. 没有证明它会确保它是情况 2.,并且您将不得不再次寻找错误...

        【讨论】:

          【解决方案6】:

          我认为编译器改变了

          positionIndex[i]++;
          if ( positionIndex[i] < endIndex[i] )
          

          变成更优化的东西,比如

          if ( positionIndex[i]++ < endIndex[i] )   <-- see my comment below, this statement is wrong.
          

          所以

          if ( positionIndex[i] + 1 < endIndex[i] )
          

          在溢出的情况下会导致未定义的行为(感谢 Seth)。

          【讨论】:

          • 杰克 - 你的意思是如果 'positionIndex[i] + 1' 溢出,那么比较显然会无效?有关如何避免/抑制此警告的任何建议?
          • 是的,如果 positionIndex 溢出它将变成一个负数,即使不是这样,这也会使比较为真。你需要负指数吗?否则你可以把它们变成未签名的。
          • 不,导致有符号类型溢出是未定义的行为,而不是像无符号类型那样环绕。
          • @Jack - 不,但实际上索引永远不会接近该值。 (可能更像是从 -1000 到 1000 哈哈),而且,它是否应该总是为那个极端的极端情况产生警告?这就是为什么我想知道是否有话要说“不要在这里警告我”,或者更好的是,对代码进行更改以实际修复警告,因为很难在这些海洋中挑选出真正的警告我得到了。
          • 因为某些原因可能是 itkImageRegionConstIteratorWithIndex.hxx 中的m_PositionIndex == m_EndIndex。在这种情况下,警告会更有意义。
          猜你喜欢
          • 1970-01-01
          • 2011-11-18
          • 2022-01-23
          • 1970-01-01
          • 1970-01-01
          • 2019-09-23
          • 2021-03-03
          • 2019-01-23
          • 1970-01-01
          相关资源
          最近更新 更多