您将在文章了解 C/C++ 中的整数溢出的第I. Introduction
节中找到下面引用的文本(重点是我的(:
通过使用 修改了编译器以插入运行时检查。但是,可靠 检测溢出错误非常困难,因为 溢出行为并不总是错误。C 和 C 的低级性质 C++意味着对象的位级和字节级操作是 司空见惯;数学运算和位级运算之间的界限 通常可能很模糊。使用无符号整数的环绕行为 合法且定义明确,并且有故意的代码习语 使用它。另一方面,C 和 C++ 具有未定义的语义 有符号溢出和移位超过位宽:完美操作 在其他语言(如Java(中定义良好。C/C++程序员是 并不总是了解有符号类型与无符号类型的不同规则 在 C 中,并且可能会天真地在有意的环绕中使用有符号类型 操作。1如果这种用途很少见,则基于编译器的溢出检测 将是执行整数错误检测的合理方法。如果是 然而,这种方法并不罕见,而且更不切实际 需要复杂的技术来区分故意 从无意中使用。
我不明白为什么基于编译器的检测对于检测有符号类型的环绕操作是不切实际的,如果这种用途并不罕见?另外,为什么我们需要区分有意和无意的使用?两者都是标准未定义的行为。
在运行时检测有符号整数溢出没有问题。像 Swift 这样的新语言可以自动可靠地做到这一点。
问题是:尽管整数溢出在 C 和 C++ 中是未定义的行为,但有大量代码会发生整数溢出,并且由于编译器静默忽略整数溢出,因此一切正常。
如果开始检测整数溢出,则此类使用将中断应用程序。当然,当开发人员运行应用程序或测试人员运行它时,这些溢出不会发生,但只有当程序交付给客户时,如果他们的应用程序在最不合适和最昂贵的时间崩溃,他们会非常非常生气,只是因为你决定禁止一些工作正常的未定义行为。
对于编译器来说,在编译时检测溢出,除了最微不足道的情况之外,都需要编译器考虑可能影响变量的每个可能的输入,并计算可能产生的每个可能的值。
显然,这是不现实的。
利用溢出的一个例子是将副作用用于其他内容。下面是一个人为的环形缓冲区示例:
int main()
{
uint8 index = 8;
char keys[256];
init_keys(keys); // Put single chars in the array
while(1) {
int letter;
letter = getc();
letter ^= keys[index];
index ++;
printf("Encoded: %cn", letter);
}
}
在这个例子中,我们创建一个 8 位整数,它必须在255+1 处溢出。我们正在利用这种溢出直接实现具有此值的环形缓冲区,而不是使用模量,后者会更典型。
有 5 种合理的方法可以处理溢出,无论是有符号的还是无符号的:
- 陷阱。通常需要额外的说明。
- 饱和。本机很少可用,通常需要额外的说明。
- 环绕式。始终本机可用于除非 2s 补码签名类型之外的所有内容。用于无符号类型。
- 未定义的行为。始终本机可用,并允许编译器做出优化假设。用于有符号类型。
- 任意结果。始终以本机方式提供。仅当环绕本身不可用时才有趣。这比UB弱,这既是它最大的优势,也是它最大的缺点。
UB 适用于优化,陷阱用于错误检测,环绕和饱和有时是需要的行为。
任意结果是填补了环绕昂贵但不需要完整 UB 的空白。
现在,有时编译器可以证明操作不会溢出,因此它不需要处理这种情况。通常用于循环计数器等,因此额外的工作并不像看起来那么多。但是,即使使用完整的源代码,跟踪数据可以具有的值也不是完美的,并且在允许的情况下,内联的障碍(如单独的编译和语义插入(可能使其无法实现。