不明白"assuming signed overflow"警告

Don't understand "assuming signed overflow" warning

本文关键字:overflow 警告 signed assuming 明白      更新时间:2023-10-16

我得到:

警告:假设 (X + C)

在这一行上:

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

类型为itk::Index(http://www.itk.org/Doxygen/html/classitk_1_1Index.html) 的m_PositionIndexm_EndIndex及其operator[]返回signed long

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

谁能解释一下是什么会导致这里的警告?我在任何地方都没有看到(x+c) < 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 的警告。

要仅禁用此警告,请使用-Wno-strict-overflow要改为禁用触发此警告的特定优化,请使用-fno-strict-overflow-fwrapv

gcc 手册页描述了可以使用级别控制此警告:-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,但我不确定为什么这会有所不同。 修复它的另一件事是使循环计数足够大,以至于循环不会被展开,但这不是一个有用的解决方案。

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

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


哲学。。。

在 [C++11:N3242$5.0.4] 中,它指出...

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

这意味着符合要求的编译器可以简单地假设溢出永远不会发生(即使对于无符号类型也是如此)。

在C++中,由于模板和复制构造函数等功能,省略无意义的操作是一项重要的优化器功能。 但有时,如果您将C++用作低级"系统语言",您可能只是希望编译器执行您告诉它要做的事情 - 并依赖于底层硬件的行为。 鉴于标准的语言,我不确定如何以独立于编译器的方式实现这一目标。

编译器告诉您,它对该代码段有足够的静态知识,知道如果它可以优化测试,则测试将始终成功,假设没有签名操作会溢出。

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

如果要禁止显示警告,请删除测试。

尽管你的问题已经很老了,但由于你还没有改变代码的那部分,我认为问题仍然存在,你仍然没有得到一个有用的解释关于这里实际发生了什么,所以让我试试吧:

在 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 %dn",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,德语为"Is täte Fefe" - "Fefe 会做什么",Fefe 是 Felix von Leitner 的昵称,我借用了代码示例)。哦,顺便说一句,是泰特菲吗?右:在 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 %dn" 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 > a总是正确的。因为我知道这一点,我知道断言永远不会触发。因为我知道这一点,所以我可以在"死代码消除"优化中消除整个断言。

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

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

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

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

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

此警告节省了我调试的时间。 谢谢,海湾合作委员会!!

这几乎可以肯定是一个错误,尽管我无法分辨这是三个可能的错误中的哪一个。

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

这很糟糕。绝对。为什么?嗯,正好有三种可能性:

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

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

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

真正糟糕的是,你绝对需要找出代码无法访问的确切原因,或者你需要反驳 gcc 是正确的(说起来容易做起来难)。根据墨菲定律,假设案例1。或 3.如果不证明它将确保它是案例 2.,并且您将不得不第二次寻找该错误......

我认为编译器发生了变化

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)。