容易被忽略的有符号数溢出问题

GCC开启-O2/O3优化后, 如果不考虑有符号数的溢出, 可能会造成一些无法理解的问题.

实例

实际举个例子, 如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// main.c
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

int main() {
for (int i = 0; i < 10; ++i) {
int32_t a = rand() % 0x10000;
int16_t b = (int16_t)((a * a) >> 16);
if (b > 0) {
printf("b > 0, b = %d\n", b);
} else {
printf("b < 0, b = %d\n", b);
}
}
}

采用不同的优化等级编译并运行:

1
2
3
4
5
6
7
8
9
10
11
❯ gcc -O0 -o main main.c && ./main
b > 0, b = 4816
b > 0, b = 1279
b > 0, b = 23228
b > 0, b = 5248
b < 0, b = -16997
b > 0, b = 8648
b > 0, b = 21989
b > 0, b = 7907
b > 0, b = 970
b > 0, b = 15575
1
2
3
4
5
6
7
8
9
10
11
❯ gcc -O2 -o main main.c && ./main
b > 0, b = 4816
b > 0, b = 1279
b > 0, b = 23228
b > 0, b = 5248
b > 0, b = -16997
b > 0, b = 8648
b > 0, b = 21989
b > 0, b = 7907
b > 0, b = 970
b > 0, b = 15575

不难发现, O2优化的结果中, 有时候b打印出来明明是负数, 但判断的时候却是始终认为其是大于0的, 这就让人感觉非常匪夷所思了.

原因分析

a是一个[0, 65535)的随机数, a * aint32_t类型的表示范围内有可能会溢出, 即超出int32_t的表示范围.
而且编译器在开启-O2/O3优化的时候, 会假设不会有有符号数溢出, 因此a * a必定大于0. 在这样一个前提下, 下面关于b的判断就会始终认为b是大于0的. 但实际上在有符号数溢出的情况下, b的值是有可能小于0的, 这样一来就导致了上面实际测试中遇到的问题.

解决办法

GCC有很多Undefined Behavior(UB), 而且有Undefined Behavior Sanitizer (UBSan) 可以在运行时发现这些隐患. 在编译的时候加上 -fsanitize=undefined 选项, 可以参考GCC文档 -fsanitize=undefined, 这样一来运行时就会有如下的错误提示:

1
2
3
4
5
6
7
8
9
10
11
12
❯ gcc -O2 -fsanitize=undefined -o main main.c && ./main
b > 0, b = 4816
b > 0, b = 1279
b > 0, b = 23228
b > 0, b = 5248
main.c:30:30: runtime error: signed integer overflow: 56401 * 56401 cannot be represented in type 'int'
b < 0, b = -16997
b > 0, b = 8648
b > 0, b = 21989
b > 0, b = 7907
b > 0, b = 970
b > 0, b = 15575

参考连接