gcc是否会跳过此检查以查找有符号整数溢出?

时间:2014-08-06 03:06:53

标签: c++ gcc compiler-construction undefined-behavior integer-overflow

例如,给出以下代码:

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 ++编译器中是否存在同样的问题,如果我不小心我的整数溢出检查不会被跳过。

1 个答案:

答案 0 :(得分:8)

简答

编译器是否肯定会优化您的示例中的检查,我们不能对所有情况说明,但我们可以使用godbolt interactive compiler使用以下代码对gcc 4.9进行测试( see it live 的):

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

长答案

当您的代码展示undefined behavior或依赖于潜在的未定义行为(此示例中的有符号整数溢出)然后是,编译器可以进行假设并围绕它们进行优化。例如,它可以假设没有未定义的行为,因此根据该假设进行优化。最臭名昭着的例子可能是removal of a null check in the Linux kernel。代码如下:

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

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

  

问题是第2行中s的取消引用允许编译器   推断s不为null(如果指针为null则为函数   未定义;编译器可以简单地忽略这种情况)。就这样   第3行中的null check得到了静默优化,现在是内核   如果攻击者可以找到一种方法来调用,则包含可利用的漏洞   这段代码带有空指针。

这同样适用于C和C ++,它们都具有围绕未定义行为的类似语言。在这两种情况下,标准都告诉我们未定义行为的结果是不可预测的,尽管两种语言中具体未定义的结果可能不同。 draft C++ standard定义了未定义的行为,如下所示:

  

本国际标准没有要求的行为

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

  

本国际标准可能会出现未定义的行为   省略任何明确的行为定义或程序使用时   错误的构造或错误的数据。 允许的未定义行为   范围从完全忽略情况与不可预测   结果,在翻译或程序执行期间表现   记录的环境特征(有或没有   发布诊断消息),终止翻译或   执行(发出诊断消息)。很多错误   程序结构不会产生未定义的行为;他们是   需要被诊断出来。

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

正确的签名溢出检查

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

#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中,我们可以看到检查被省略了,这是我们期望的行为。