简短回答
在你的示例中,编译器是否一定会优化掉检查,我们不能对所有情况做出断言,但我们可以使用以下代码针对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 ..
使用的逻辑是,由于已经解除了对
的引用,因此它必须不是空指针,否则行为将未定义,因此优化掉了if(!s)检查。链接的文章说:
问题在于第2行中对s的解引用允许编译器推断出s不是null(如果指针为null,则函数未定义;编译器可以简单地忽略此情况)。因此,在第3行中的null检查被静默地优化掉,现在如果攻击者能够找到一种方法来使用null指针调用此代码,内核将包含可利用的错误。
这同样适用于C和C++,它们都有类似于未定义行为的语言。在两种情况下,标准告诉我们未定义行为的结果是不可预测的,尽管在任一语言中具体未定义的内容可能有所不同。draft C++ standard定义了未定义行为如下:
本国际标准没有规定的行为
并包括以下注释(我强调):
当国际标准省略任何明确的行为定义或程序使用错误构造或错误数据时,可能会出现未定义行为。允许的未定义行为范围从完全忽略情况并产生不可预测的结果,到在翻译或程序执行期间以特定于环境的记录方式行事(无论是否发出诊断消息),直至终止翻译或执行(并发出诊断消息)。许多错误的程序结构不会引起未定义的行为;它们需要被诊断。
草案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)))) {
} else {
sum = si_a + si_b;
}
如果我们将这段代码插入到godbolt中,我们可以看到检查被省略了,这是我们所期望的行为。
x+1>x
始终成立,或者让您断言循环迭代是有限的。 - Damon