考虑以下代码(
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
...
assert(hi >= 0 && hi < 64); assert(lo >= 0 && lo <= hi);
。我认为问题不在于u+3
,而是这两个断言有5个测试,而只需要4个。 - chux - Reinstate Monicaint8_t
进行算术运算,lo + 3
的加法也永远不会溢出。 - Jonathan Leffler