未定义的行为怪癖:在缓冲区外读取导致循环永远不会终止

Undefined Behavior quirk: reading outside a buffer causes a loop to never terminate?

本文关键字:读取 循环 永远 终止 缓冲区 未定义      更新时间:2023-10-16

我写了一个非常琐碎的程序,试图检查附加到缓冲区溢出的未定义行为。具体来说,关于在分配的空间之外对数据执行读取时会发生什么。

#include <iostream>
#include<iomanip>
int main() {
int values[10];
for (int i = 0; i < 10; i++) {
values[i] = i;
}
std::cout << values << " ";
std::cout << std::endl;
for (int i = 0; i < 11; i++) {
//UB occurs here when values[i] is executed with i == 10
std::cout << std::setw(2) << i << "(" << (values + i) << "): " << values[i] << std::endl;
}
system("pause");
return 0;
}

当我在Visual Studio上运行这个程序时,结果并不令人惊讶:读取索引10会产生垃圾:

000000000025FD70 
0(000000000025FD70): 0
1(000000000025FD74): 1
2(000000000025FD78): 2
3(000000000025FD7C): 3
4(000000000025FD80): 4
5(000000000025FD84): 5
6(000000000025FD88): 6
7(000000000025FD8C): 7
8(000000000025FD90): 8
9(000000000025FD94): 9
10(000000000025FD98): -1966502944
Press any key to continue . . . 

但当我把这个程序输入Ideone.com的在线编译器时,我得到了非常奇怪的行为:

0xff8cac48 
0(0xff8cac48): 0
1(0xff8cac4c): 1
2(0xff8cac50): 2
3(0xff8cac54): 3
4(0xff8cac58): 4
5(0xff8cac5c): 5
6(0xff8cac60): 6
7(0xff8cac64): 7
8(0xff8cac68): 8
9(0xff8cac6c): 9
10(0xff8cac70): 1
11(0xff8cac74): -7557836
12(0xff8cac78): -7557984
13(0xff8cac7c): 1435443200
14(0xff8cac80): 0
15(0xff8cac84): 0
16(0xff8cac88): 0
17(0xff8cac8c): 1434052387
18(0xff8cac90): 134515248
19(0xff8cac94): 0
20(0xff8cac98): 0
21(0xff8cac9c): 1434052387
22(0xff8caca0): 1
23(0xff8caca4): -7557836
24(0xff8caca8): -7557828
25(0xff8cacac): 1432254426
26(0xff8cacb0): 1
27(0xff8cacb4): -7557836
28(0xff8cacb8): -7557932
29(0xff8cacbc): 134520132
30(0xff8cacc0): 134513420
31(0xff8cacc4): 1435443200
32(0xff8cacc8): 0
33(0xff8caccc): 0
34(0xff8cacd0): 0
35(0xff8cacd4): 346972086
36(0xff8cacd8): -29697309
37(0xff8cacdc): 0
38(0xff8cace0): 0
39(0xff8cace4): 0
40(0xff8cace8): 1
41(0xff8cacec): 134514984
42(0xff8cacf0): 0
43(0xff8cacf4): 1432277024
44(0xff8cacf8): 1434052153
45(0xff8cacfc): 1432326144
46(0xff8cad00): 1
47(0xff8cad04): 134514984
... 
//The heck?! This just ends with a Runtime Error after like 200 lines.

很明显,在他们的编译器中,用一个索引溢出缓冲区会导致程序进入无限循环!

现在,重申一下:我意识到我在这里处理的是未定义的行为。尽管如此,我还是想知道幕后到底发生了什么。物理上执行缓冲区溢出的代码仍然执行4个字节的读取,并将读取的内容写入(可能保护得更好)缓冲区。编译器/CPU是做什么导致这些问题的?

有两条执行路径导致正在评估的条件i < 11

第一个是在初始循环迭代之前。由于i在检查之前已经初始化为0,所以这是非常正确的。

第二个是在成功的循环迭代之后。由于循环迭代导致values[i]被访问,而values只有10个元素,因此这只能在i < 10的情况下有效。如果i < 10,在i++之后,i < 11也必须为真。

这就是Ideone的编译器(GCC)正在检测到的。条件i < 11不可能为假,除非你有一个无效的程序,因此它可以被优化掉。同时,编译器不会特意检查你是否有一个无效的程序,除非你提供额外的选项来告诉它这样做(比如GCC/clang中的-fsanitize=undefined)。

这是实现必须做出的权衡。他们可以支持无效程序的可理解行为,也可以支持有效程序的原始速度。或者两者兼而有之。GCC肯定非常关注后者,至少在默认情况下是这样。