gcc会跳过对有符号整数溢出的检查吗?

Will gcc skip this check for signed integer overflow?

本文关键字:溢出 检查 整数 符号 gcc      更新时间:2023-10-16

例如,给定以下代码:

int f(int n)
{
    if (n < 0)
        return 0;
    n = n + 100;
    if (n < 0)
        return 0;
    return n;
}

假设你传递的数字非常接近整数溢出(小于100),编译器会产生代码,会给你一个负返回?

Simon Tatham的《The Descent to C》节选如下:

" GNU C编译器(gcc)为这个函数生成代码,它可以返回一个负整数,如果你传入(例如)最大可表示的' int '值。因为编译器在第一个if语句之后知道n是正数,然后它假设没有发生整数溢出,并使用该假设得出结论,即加法后的n值必须仍然是正数,所以它完全删除第二个if语句并返回未检查的加法结果。"

这让我想知道是否同样的问题存在于c++编译器中,如果我应该小心,我的整数溢出检查不被跳过。

简短回答

编译器肯定会优化你的例子中的检查,我们不能说所有的情况下,但我们可以做一个测试对gcc 4.9使用godbolt交互式编译器与以下代码(看到它现场):

int f(int n)
{
    if (n < 0) return 0;
    n = n + 100;
    if (n < 0) return 0;
    return n;
}
int f2(int n)
{
    if (n < 0) return 0;
    n = n + 100;
    return n;
}

,我们看到它为两个版本生成相同的代码,这意味着它确实省略了第二个检查:

f(int):  
    leal    100(%rdi), %eax #, tmp88 
    testl   %edi, %edi  # n
    movl    $0, %edx    #, tmp89
    cmovs   %edx, %eax  # tmp88,, tmp89, D.2246
    ret
f2(int):
    leal    100(%rdi), %eax #, tmp88
    testl   %edi, %edi  # n
    movl    $0, %edx    #, tmp89 
    cmovs   %edx, %eax  # tmp88,, tmp89, D.2249
    ret

长回答

当你的代码显示未定义行为或依赖于潜在的未定义行为(在这个例子中有符号整数溢出),那么是的,编译器可以做出假设并围绕它们进行优化。例如,它可以假设没有未定义的行为,从而根据该假设进行优化。最臭名昭著的例子可能是在Linux内核中删除空检查。代码如下:

struct foo *s = ...;
int x = s->f;
if (!s) return ERROR;
... use s ..

使用的逻辑是,由于s被解引用,它必须不是空指针,否则将是未定义的行为,因此它优化了if (!s)检查。链接的文章说:

问题是第2行中对s的解引用允许编译器要推断s不为空(如果指针为空,则函数没有定义;编译器可以简单地忽略这种情况)。因此,第3行中的Null检查被默默地优化了,现在是内核如果攻击者可以找到调用的方法,则包含可利用的错误这段代码带有一个空指针。

这同样适用于C和c++,它们都有类似的关于未定义行为的语言。在这两种情况下,标准都告诉我们,未定义行为的结果是不可预测的,尽管两种语言中具体未定义的内容可能不同。c++标准草案对未定义行为的定义如下:

本国际标准未规定的行为

,并包括以下注释(强调我的):

在本国际标准中可能会出现未定义的行为省略行为的任何显式定义,或者当程序使用错误的结构或错误的数据。允许的未定义行为从完全忽视情况到不可预测. results表示在转换或程序执行期间的行为环境特征的文件化方式(有或没有)(发出诊断消息),终止翻译或执行(发出诊断消息)。很多错误的程序构造不会产生未定义的行为;他们是需要诊断

C11标准草案有类似的语言。

正确的带符号溢出检查

您的检查不是防止有符号整数溢出的正确方法,您需要在执行操作之前检查,如果执行操作会导致溢出,则不要执行操作。Cert有一个关于如何防止各种操作的有符号整数溢出的很好的参考。对于添加的情况,它建议如下:

#include <limits.h>
void f(signed int si_a, signed int si_b) {
  signed int sum;
  if (((si_b > 0) && (si_a > (INT_MAX - si_b))) ||
      ((si_b < 0) && (si_a < (INT_MIN - si_b)))) {
    /* Handle error */
  } else {
    sum = si_a + si_b;
  }

如果我们将这段代码插入godbolt中,我们可以看到检查被省略了,这是我们期望的行为。