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

Sai*_*nti 3 c++ compiler-construction gcc integer-overflow undefined-behavior

例如,给出以下代码:

int f(int n)
{
    if (n < 0)
        return 0;
    n = n + 100;
    if (n < 0)
        return 0;
    return n;
}
Run Code Online (Sandbox Code Playgroud)

假设您传入的数字非常接近整数溢出(小于100),编译器是否会生成会给您带来负回报的代码?

以下是Simon Tatham的"The Descent to C"中关于这个问题的摘录:

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

它让我想知道C++编译器中是否存在同样的问题,如果我不小心我的整数溢出检查不会被跳过.

Sha*_*our 8

简答

编译器是否肯定会优化你的示例中的检查,我们不能说所有情况,但我们可以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;
}
Run Code Online (Sandbox Code Playgroud)

并且我们看到它为两个版本生成相同的代码,这意味着它确实在第二次检查时丢失:

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
Run Code Online (Sandbox Code Playgroud)

答案很长

当您的代码显示未定义的行为或依赖于潜在的未定义行为(在此示例中为有符号整数溢出)然后是,编译器可以进行假设并围绕它们进行优化.例如,它可以假设没有未定义的行为,因此根据该假设进行优化.最臭名昭着的例子可能是删除Linux内核中的空检查.代码如下:

struct foo *s = ...;
int x = s->f;
if (!s) return ERROR;
... use s ..
Run Code Online (Sandbox Code Playgroud)

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

问题是第2行中s的取消引用允许编译器推断s不是null(如果指针为null则函数未定义;编译器可以简单地忽略这种情况).因此,第3行中的空检查会被静默优化,如果攻击者能够找到使用空指针调用此代码的方法,则内核包含可利用的错误.

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

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

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

当本国际标准忽略任何明确的行为定义或程序使用错误的构造或错误数据时,可能会出现未定义的行为.允许的未定义行为包括完全忽略不可预测的结果,在翻译或程序执行期间以环境特征(有或没有发出诊断消息)的特定行为,终止翻译或执行(发布时)一条诊断信息).许多错误的程序结构不会产生未定义的行为; 他们需要被诊断出来.

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;
  }
Run Code Online (Sandbox Code Playgroud)

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