为什么GCC 4.8.2会抱怨在严格溢出模式下的加法运算?

8
考虑以下代码(bits.c):
#include <assert.h>
#include <inttypes.h>
#include <stdio.h>

static uint64_t pick_bits(unsigned char *bytes, size_t nbytes, int lo, int hi)
{
  assert(bytes != 0 && nbytes > 0 && nbytes <= 8);
  assert(lo >= 0 && lo < 64);
  assert(hi >= 0 && hi < 64 && hi >= lo);
  uint64_t result = 0;
  for (int i = nbytes - 1; i >= 0; i--)
    result = (result << 8) | bytes[i];
  result >>= lo;
  result &= (UINT64_C(1) << (hi - lo + 1)) - 1;
  return result;
}

int main(void)
{
  unsigned char d1[8] = "\xA5\xB4\xC3\xD2\xE1\xF0\x96\x87";
  for (int u = 0; u < 64; u += 4)
  {
    uint64_t v = pick_bits(d1, sizeof(d1), u, u+3);
    printf("Picking bits %2d..%2d gives 0x%" PRIX64 "\n", u, u+3, v);
  }
  return 0;
}

当使用GCC 4.8.2编译(适用于Ubuntu 12.04衍生版本)时,启用严格警告:

$ gcc -g -O3 -std=c99 -Wall -Wextra -Wmissing-prototypes -Wstrict-prototypes \
>     -Wold-style-definition -Wold-style-declaration -Werror  bits.c -o bits
In file included from bits.c:1:0:
bits.c: In function ‘main’:
bits.c:9:35: error: assuming signed overflow does not occur when assuming that (X + c) < X is always false [-Werror=strict-overflow]
   assert(hi >= 0 && hi < 64 && hi >= lo);
                                   ^
cc1: all warnings being treated as errors

我感到困惑:GCC怎么会抱怨一个加法?那一行没有任何加法(即使在预处理后)!预处理输出的相关部分如下:

# 4 "bits.c" 2

static uint64_t pick_bits(unsigned char *bytes, size_t nbytes, int lo, int hi)
{
  ((bytes != 0 && nbytes > 0 && nbytes <= 8) ? (void) (0) : __assert_fail ("bytes != 0 && nbytes > 0 && nbytes <= 8", "bits.c", 7, __PRETTY_FUNCTION__));
  ((lo >= 0 && lo < 64) ? (void) (0) : __assert_fail ("lo >= 0 && lo < 64", "bits.c", 8, __PRETTY_FUNCTION__));
  ((hi >= 0 && hi < 64 && hi >= lo) ? (void) (0) : __assert_fail ("hi >= 0 && hi < 64 && hi >= lo", "bits.c", 9, __PRETTY_FUNCTION__));
  uint64_t result = 0;
  for (int i = nbytes - 1; i >= 0; i--)
    result = (result << 8) | bytes[i];
  result >>= lo;
  result &= (1UL << (hi - lo + 1)) - 1;
  return result;
}

显然,我可以添加-Wno-strict-overflow来抑制该警告,但我不明白为什么首先会认为该警告适用于此代码。

(我注意到错误据说是In function ‘main’:,但这是因为它能够将函数的代码积极地内联到main中。)


进一步观察

回答引发了一些观察:

  • 问题是由于内联而发生的。
  • 删除static不足以避免问题。
  • 单独编译函数与main一起工作。
  • 添加__attribute__((noinline))也有效。
  • 使用-O2优化也可以避免这个问题。

附属问题

这对我来说看起来像GCC编译器的可疑行为。

  • 值得向GCC团队报告可能的错误吗?

汇编输出

命令:

$ gcc -g -O3 -std=c99 -Wall -Wextra -Wmissing-prototypes -Wstrict-prototypes \
> -Wold-style-definition -Wold-style-declaration -Werror -S \
> -Wno-strict-overflow bits.c
$

汇编语言(顶部部分):

    .file   "bits.c"
    .text
.Ltext0:
    .section    .rodata.str1.8,"aMS",@progbits,1
    .align 8
.LC0:
    .string "Picking bits %2d..%2d gives 0x%lX\n"
    .section    .text.startup,"ax",@progbits
    .p2align 4,,15
    .globl  main
    .type   main, @function
main:
.LFB8:
    .file 1 "bits.c"
    .loc 1 19 0
    .cfi_startproc
.LVL0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
.LBB8:
.LBB9:
    .loc 1 23 0
    movl    $3, %edx
.LBB10:
.LBB11:
    .loc 1 13 0
    movabsq $-8676482779388332891, %rbp
.LBE11:
.LBE10:
.LBE9:
.LBE8:
    .loc 1 19 0
    pushq   %rbx
    .cfi_def_cfa_offset 24
    .cfi_offset 3, -24
.LBB22:
    .loc 1 21 0
    xorl    %ebx, %ebx
.LBE22:
    .loc 1 19 0
    subq    $8, %rsp
    .cfi_def_cfa_offset 32
    jmp .L2
.LVL1:
    .p2align 4,,10
    .p2align 3
.L3:
    leal    3(%rbx), %edx
.LVL2:
.L2:
.LBB23:
.LBB20:
.LBB16:
.LBB12:
    .loc 1 13 0
    movl    %ebx, %ecx
    movq    %rbp, %rax
.LBE12:
.LBE16:
    .loc 1 24 0
    movl    %ebx, %esi
.LBB17:
.LBB13:
    .loc 1 13 0
    shrq    %cl, %rax
.LBE13:
.LBE17:
    .loc 1 24 0
    movl    $.LC0, %edi
.LBE20:
    .loc 1 21 0
    addl    $4, %ebx
.LVL3:
.LBB21:
.LBB18:
.LBB14:
    .loc 1 13 0
    movq    %rax, %rcx
.LBE14:
.LBE18:
    .loc 1 24 0
    xorl    %eax, %eax
.LBB19:
.LBB15:
    .loc 1 14 0
    andl    $15, %ecx
.LBE15:
.LBE19:
    .loc 1 24 0
    call    printf
.LVL4:
.LBE21:
    .loc 1 21 0
    cmpl    $64, %ebx
    jne .L3
.LBE23:
    .loc 1 27 0
    addq    $8, %rsp
    .cfi_def_cfa_offset 24
    xorl    %eax, %eax
    popq    %rbx
    .cfi_def_cfa_offset 16
.LVL5:
    popq    %rbp
    .cfi_def_cfa_offset 8
    ret
    .cfi_endproc
.LFE8:
    .size   main, .-main
    .text
...

值得向GCC团队报告作为可能的错误吗?您认为什么是错误?如果您要求将所有警告视为错误,则GCC会生成错误。它发出警告是因为它正在生成假定没有溢出发生的代码。如果您不想看到该警告,可以通过某种方法禁用它。 - AProgrammer
备选方案:assert(hi >= 0 && hi < 64); assert(lo >= 0 && lo <= hi);。我认为问题不在于 u+3,而是这两个断言有5个测试,而只需要4个。 - chux - Reinstate Monica
@AProgrammer:我认为的错误是GCC没有注意到溢出不可能发生,因为这些值受到如此严格的限制,即使使用int8_t进行算术运算,lo + 3的加法也永远不会溢出。 - Jonathan Leffler
@chux:不幸的是,您提出的更改并未改变结果。 - Jonathan Leffler
@chux:遗憾的是,仅检查“lo”和“hi”的单个断言也不会改变结果;它仍然会生成错误。 - Jonathan Leffler
显示剩余10条评论
2个回答

7
它正在内联函数,然后生成错误。你可以自己看到:

它正在内联函数,然后生成错误。你可以亲自验证:

__attribute__((noinline))
static uint64_t pick_bits(unsigned char *bytes, size_t nbytes, int lo, int hi)

在我的系统上,原始版本会生成相同的警告,但是`noinline` 版本不会。
因为它实际上是 `u+3 >= u`,所以 GCC 优化掉了 `hi >= lo`,并生成了一个警告,因为它不能很好地判断 `u+3` 是否会溢出。真是太遗憾了。
文档
从 GCC 文档 3.8 节中得知:
“假设没有发生带符号溢出的优化是完全安全的,如果涉及的变量的值从未导致溢出事实上发生。 因此,这个警告很容易产生误报:有关实际上不是问题的代码的警告。为了帮助集中注意力,定义了几个警告级别。特别地,在估计循环需要多少次迭代时以及确定是否根本执行循环时使用未定义的带符号溢出不发出任何警告。”
我个人的建议是使用 `-Wno-error=strict-overflow`,或者在有问题的代码中使用 `#pragma` 来禁用警告。

仅仅移除static是不够的。将函数与main分开编译可以解决问题,添加__attribute__((noinline))也有效。使用-O2优化也可以避免这个问题。附带问题:是否值得向GCC团队报告此问题作为一个bug? - Jonathan Leffler
1
最糟糕的情况是他们会告诉你假阳性是不可避免的,你应该忽略它或关闭它。这个警告经常是一个假阳性,这一事实已经写在GCC文档中了。我倾向于有选择地关闭警告。静态分析很难。 - Dietrich Epp
我对两个问题感到困惑:1. 当然,通过内联编译器可以看到比我更多的内容,但为什么要将警告放在pick_bits()中的assert而不是在真正发生加法的调用点(pick_bits(d1, sizeof(d1), u, u+3);)?2. pick_bits的作者不会因为调用点出现问题而删除合理的assert。使用#pragma GCC diagnostic push、#pragma GCC diagnostic ignored“-Wstrict-overflow”和#pragma GCC diagnostic pop这些荒谬的指示来抑制错误。 - pdbj
真的吗?使用strict-overflow查找实际问题的唯一方式是这样吗?在代码中加入五行预处理器以解决调用者提出的问题吗? - pdbj
@pdbj:未定义溢出通常很混乱,C语言通常也很混乱,在这种混乱的环境中,除非你不遗余力地避免它们,否则你将不可避免地收到错误的正面警告。警告在pick_bits内生成的原因是因为在pick_bits内的代码触发了错误:hi >= lo是触发代码。hi = lo + 3的定义是上下文的一部分。我认为,如果您想完全避免#ifdef,除非您的程序很简单,否则您必须放弃C或可移植性之一。 - Dietrich Epp
@pdbj:也许在未来的某个时候,GCC将能够识别允许其推断出hi >= low可以被优化掉的上下文片段。Clang静态分析器可以做到这样的事情。 - Dietrich Epp

6
我的假设是gcc正在内联pick_bits函数,并使用hi == lo+3的知识进行编译,这使得它可以假设只要lo足够低,以至于lo+3不会溢出,则hi >= lo始终为真。

+1,这也是我的直觉。我猜测-O3会包含激进的内联。 - Carl Norum
4
从技术上讲,可以假设 lo+3 不会溢出,因为 C 标准是这样规定的,即使 lo = INT_MAX - Dietrich Epp
@DietrichEpp,同意,gcc可能会警告代码假定没有溢出,并且用户要求gcc将所有警告视为错误。 - AProgrammer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接