例如,给出以下代码:
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++ 编译器中是否存在同样的问题,以及我是否应该小心不要跳过整数溢出检查。
简短回答
编译器是否一定会优化示例中的检查,我们不能说适用于所有情况,但我们可以使用
godbolt 交互式编译器和以下代码对
gcc 4.9
进行测试(现场查看) ):
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 行中的空检查被悄悄优化掉,现在内核 如果攻击者能够找到调用方法,则包含可利用的错误 这段代码带有一个空指针。
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,我们可以看到检查被省略,这是我们所期望的行为。