【问题标题】:Will gcc skip this check for signed integer overflow?gcc 会跳过这个检查有符号整数溢出吗?
【发布时间】:2014-08-06 03:06:53
【问题描述】:

例如,给定以下代码:

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++ 编译器中是否存在相同的问题,以及是否应该小心不要跳过整数溢出检查。

【问题讨论】:

  • 当您有未定义的行为时,编译器可能会做出看起来很奇怪的假设,但您查看此example here 会看到编译器由于围绕未定义行为的优化而将有限循环变成了无限循环。
  • 这里有趣的是,该标准故意将在硬件上(无论如何在所有真实,非虚构硬件上)完全定义好的东西定义为仅未定义的行为所以编译器可以做这种优化。尽管 INT_MAX+1 在您将能够找到的任何 CPU 上确实等于 INT_MIN,但假设它没有并说这是未定义的,您可以合法地优化上述代码或让您考虑将x+1&gt;x 视为“始终正确”,或者让您断言循环迭代是有限的。
  • 也许他们觉得这个问题之前已经得到了充分的回答,而我没有很好地搜索到它?
  • @Damon:你这个断言的依据是什么? 1989 年,大多数硬件上的行为是一致的,但不是全部,标准的作者在基本原理中指出,使短无符号值提升为有符号的主要动机因素是,大多数实现都定义了静默环绕溢出语义。

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


【解决方案1】:

简答

编译器是否肯定会优化您示例中的检查,我们不能说适用于所有情况,但我们可以使用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 不为空(如果指针为空,则函数 未定义;编译器可以简单地忽略这种情况)。就这样 第 3 行中的空值检查被静默优化,现在内核 如果攻击者可以找到调用方法,则包含可利用的错误 这段代码带有一个空指针。

这同样适用于 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,我们可以看到检查被省略,这是我们所期望的行为。

【讨论】:

  • FWIW,使用g++ -O3 对其进行了优化。 generated code 与为 int f(int n) { return n &lt; 0? 0 : n + 100; } 生成的代码相同。
  • @T.C.我之前没有机会添加 Godbolt 的测试结果,但确实如此。
  • @JeremyFriesner 我同意你的观点,正如我在my comment here 中所说的,这涉及到类似的问题。虽然在那种情况下编译器确实警告了未定义的行为,但在许多情况下它并没有,我不完全理解。
  • 我想知道允许溢出产生不受约束的行为所带来的效率提升与必须使用上述代码来执行原本简单的“返回 si_a+si_b;”的成本相比如何?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-03-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-02-29
  • 2016-08-23
相关资源
最近更新 更多