有符号整数溢出(严格来说,“无符号整数溢出”并不存在)意味着未定义行为。这意味着任何事情都可能发生,在C++规则下讨论它为什么会发生是没有意义的。
C ++ 11草案N3337:§5.4: 1
如果在表达式的评估过程中,结果在其类型的表示范围内不能被数学上定义或表示,则其行为是未定义的。[注意:大多数现有的C ++实现忽略整数溢出。除以零,使用零除数形成余数以及所有浮点异常的处理因机器而异,并且通常可以通过库函数进行调整。—end note]
即使没有-Wall
选项,您的代码使用g++ -O3
编译时也会发出警告。
a.cpp: In function 'int main()':
a.cpp:11:18: warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
std::cout << i*1000000000 << std::endl;
^
a.cpp:9:2: note: containing loop
for (int i = 0; i < 4; ++i)
^
唯一的分析程序正在做什么的方法是通过阅读生成的汇编代码。
以下是完整的汇编清单:
.file "a.cpp"
.section .text$_ZNKSt5ctypeIcE8do_widenEc,"x"
.linkonce discard
.align 2
LCOLDB0:
LHOTB0:
.align 2
.p2align 4,,15
.globl __ZNKSt5ctypeIcE8do_widenEc
.def __ZNKSt5ctypeIcE8do_widenEc
__ZNKSt5ctypeIcE8do_widenEc:
LFB860:
.cfi_startproc
movzbl 4(%esp), %eax
ret $4
.cfi_endproc
LFE860:
LCOLDE0:
LHOTE0:
.section .text.unlikely,"x"
LCOLDB1:
.text
LHOTB1:
.p2align 4,,15
.def ___tcf_0
___tcf_0:
LFB1091:
.cfi_startproc
movl $__ZStL8__ioinit, %ecx
jmp __ZNSt8ios_base4InitD1Ev
.cfi_endproc
LFE1091:
.section .text.unlikely,"x"
LCOLDE1:
.text
LHOTE1:
.def ___main
.section .text.unlikely,"x"
LCOLDB2:
.section .text.startup,"x"
LHOTB2:
.p2align 4,,15
.globl _main
.def _main
_main:
LFB1084:
.cfi_startproc
leal 4(%esp), %ecx
.cfi_def_cfa 1, 0
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
.cfi_escape 0x10,0x5,0x2,0x75,0
movl %esp, %ebp
pushl %edi
pushl %esi
pushl %ebx
pushl %ecx
.cfi_escape 0xf,0x3,0x75,0x70,0x6
.cfi_escape 0x10,0x7,0x2,0x75,0x7c
.cfi_escape 0x10,0x6,0x2,0x75,0x78
.cfi_escape 0x10,0x3,0x2,0x75,0x74
xorl %edi, %edi
subl $24, %esp
call ___main
L4:
movl %edi, (%esp)
movl $__ZSt4cout, %ecx
call __ZNSolsEi
movl %eax, %esi
movl (%eax), %eax
subl $4, %esp
movl -12(%eax), %eax
movl 124(%esi,%eax), %ebx
testl %ebx, %ebx
je L15
cmpb $0, 28(%ebx)
je L5
movsbl 39(%ebx), %eax
L6:
movl %esi, %ecx
movl %eax, (%esp)
addl $1000000000, %edi
call __ZNSo3putEc
subl $4, %esp
movl %eax, %ecx
call __ZNSo5flushEv
jmp L4
.p2align 4,,10
L5:
movl %ebx, %ecx
call __ZNKSt5ctypeIcE13_M_widen_initEv
movl (%ebx), %eax
movl 24(%eax), %edx
movl $10, %eax
cmpl $__ZNKSt5ctypeIcE8do_widenEc, %edx
je L6
movl $10, (%esp)
movl %ebx, %ecx
call *%edx
movsbl %al, %eax
pushl %edx
jmp L6
L15:
call __ZSt16__throw_bad_castv
.cfi_endproc
LFE1084:
.section .text.unlikely,"x"
LCOLDE2:
.section .text.startup,"x"
LHOTE2:
.section .text.unlikely,"x"
LCOLDB3:
.section .text.startup,"x"
LHOTB3:
.p2align 4,,15
.def __GLOBAL__sub_I_main
__GLOBAL__sub_I_main:
LFB1092:
.cfi_startproc
subl $28, %esp
.cfi_def_cfa_offset 32
movl $__ZStL8__ioinit, %ecx
call __ZNSt8ios_base4InitC1Ev
movl $___tcf_0, (%esp)
call _atexit
addl $28, %esp
.cfi_def_cfa_offset 4
ret
.cfi_endproc
LFE1092:
.section .text.unlikely,"x"
LCOLDE3:
.section .text.startup,"x"
LHOTE3:
.section .ctors,"w"
.align 4
.long __GLOBAL__sub_I_main
.lcomm __ZStL8__ioinit,1,1
.ident "GCC: (i686-posix-dwarf-rev1, Built by MinGW-W64 project) 4.9.0"
.def __ZNSt8ios_base4InitD1Ev
.def __ZNSolsEi
.def __ZNSo3putEc
.def __ZNSo5flushEv
.def __ZNKSt5ctypeIcE13_M_widen_initEv
.def __ZSt16__throw_bad_castv
.def __ZNSt8ios_base4InitC1Ev
.def _atexit
我几乎看不懂汇编语言,但我也能看到addl $1000000000, %edi
这一行。
生成的代码看起来更像是
for(int i = 0; ; i += 1000000000)
std::cout << i << std::endl;
@T.C.的这条评论:
我怀疑原因是:(1) 因为任何值大于2的
i的每次迭代都有未定义的行为 -> (2) 我们可以假设
i <= 2以进行优化 -> (3) 循环条件始终为真 -> (4) 它被优化成无限循环。
这启发我将OP代码的汇编代码与以下没有未定义行为的代码的汇编代码进行比较。
#include <iostream>
int main()
{
for (int i = 0; i < 3; ++i)
std::cout << i*1000000000 << std::endl;
}
实际上,正确的代码已经有了终止条件。
L6:
mov ecx, edi
mov DWORD PTR [esp], eax
add esi, 1000000000
call __ZNSo3putEc
sub esp, 4
mov ecx, eax
call __ZNSo5flushEv
cmp esi, -1294967296 // here it is
jne L7
lea esp, [ebp-16]
xor eax, eax
pop ecx
很遗憾,这是编写有漏洞代码的后果。
幸运的是,您可以利用更好的诊断和调试工具-这就是它们存在的目的:
启用所有警告
-Wall
是gcc选项,可启用所有有用的警告,没有误报。这是你应该始终使用的最低限度。
gcc还有许多其他警告选项,但它们不会随-Wall
启用,因为它们可能会产生误报。
不幸的是,Visual C ++ 在提供有用警告方面落后。至少IDE默认启用一些。
使用调试标志进行调试
- 对于整数溢出,
-ftrapv
在溢出时使程序陷入困境,
- Clang编译器非常适合此操作:
-fcatch-undefined-behavior
捕捉了许多未定义行为的实例(注意:"许多"!=“全部”
)
我有一个混乱的程序需要明天发货!救命!!!!111oneone
使用gcc的-fwrapv
此选项指示编译器假定有符号算术加法、减法和乘法溢出使用二进制补码表示进行循环。
1 - 该规则不适用于“无符号整数溢出”,因为§3.9.1.4说:
无符号整数,声明为unsigned,应遵守对该特定大小的整数值表示中的比特数取模的算术法则。
例如 UINT_MAX + 1
的结果是按照算术模2n的规则进行定义的。