【问题标题】:How to catch undefined behaviour without executing it?如何在不执行的情况下捕获未定义的行为?
【发布时间】:2014-06-21 06:59:26
【问题描述】:

在我的软件中,我在运行时使用来自用户的输入值并执行一些数学运算。为简单起见,请考虑以下示例:

int multiply(const int a, const int b)
{
    if(a >= INT_MAX || B >= INT_MAX)
        return 0;
    else
        return a*b;
}

我可以检查输入值是否大于限制,但是如何检查结果是否超出限制? a = INT_MAX - 1b = 2 很有可能。由于输入完全有效,它将执行makes my program meaningless 的未定义代码。这意味着在此之后执行的任何代码都是随机的,最终可能导致崩溃。那么在这种情况下我该如何保护我的程序呢?

【问题讨论】:

  • 对于“如何预防 UB?”没有一般的答案。对于这种特殊情况:stackoverflow.com/questions/2713972/…
  • 在这种特殊情况下,将两者都转换为long long 并检查乘法的结果是大于INT_MAX 还是小于INT_MIN 通常应该可以工作。
  • (a >= INT_MAX) 等价于 (a == INT_MAX) 如果 a 是 int
  • @Cool_Coder But how will documenting the error help? 如果用户没有阅读文档,那是他们的问题,而不是你的问题。

标签: c++ c


【解决方案1】:

这真的取决于你在这种情况下真正想要做什么。

对于 longlong long(或 int64_t)是 64 位值,int 是 32 位值的机器,你可以这样做(我假设 long 是这里是 64 位):

long x = static_cast<long>(a) * b;
if (x > MAX_INT || x < MIN_INT)
   return 0;
else
   return static_cast<int>(x);

通过将一个值转换为long,另一个也必须进行转换。如果这让你更快乐,你可以同时施放。这里的开销,高于正常的 32 位乘法是现代 CPU 上的几个时钟周期,您不太可能找到更安全的解决方案,也就是更快。 [在某些编译器中,您可以向if 添加属性,表示对于返回x 的常见情况,不太可能鼓励分支预测“使其正确”]

显然,这不适用于类型与您可以处理的最大整数一样大的值(尽管您可以使用浮点数,但它可能仍然有点狡猾,因为浮点数的精度是还不够——如果您不需要整个整数范围,可以使用一些“安全余量”来完成[例如,与小于LONG_INT_MAX / 2] 进行比较。)。这里的惩罚有点糟糕,尤其是浮点数和整数之间的转换并不“令人愉快”。

另一种选择是使用“已知无效值”实际测试相关代码,只要其余代码“正常”即可。确保使用相关的编译器设置对此进行测试,因为更改编译器选项会改变行为。请注意,您的代码必须处理“当65536 * 100000 是负数时我们该怎么做”,而您的代码没想到会这样。也许添加类似:

 int x = a * b;
 if (x < 0) return 0; 

[当然,这只有在您不期望负面结果时才有效]

您还可以检查生成的汇编代码并了解实际处理器的体系结构 [这里的关键是了解“溢出是否会陷阱”——在 x86、ARM、68K、29K 中默认情况下不会出现这种情况。我认为 MIPS 有一个“trap on overflow”的选项,并确定它是否可能导致问题 [1],并添加类似

的内容
#if (defined(__X86__) || defined(__ARM__))
 #error This code needs inspecting for correct behaviour 
#endif
    return a * b;

但是,这种方法的一个问题是,即使是代码或编译器版本中最轻微的变化也可能会改变结果,因此将其与上面的测试方法相结合很重要(并确保您测试的是实际的生产代码,不是一些被黑的小例子)。

[1] 未定义“未定义行为”以允许 C 在具有捕获整数数学溢出的处理器上“工作”,以及 a * b 在有符号值中溢出时的事实当然是除非你有一个定义好的数学系统(二进制补码,一个补码,不同的符号位),否则很难确定——所以为了避免在这些情况下“定义”确切的行为,C 标准说“它是未定义的”。这并不意味着它肯定会变坏。

【讨论】:

  • 您的解决方案很好,除了 2 种情况:1. 在 32 位机器上 LONG_MAX == INT_MAX。因此,您的解决方案将导致 UB。 2. 在任何机器上,如果结果 >LONG_MAX。除此之外,我喜欢你的技术
  • 对,我解释说“long 被假定为 64 位,int 32 位”[在我的机器上恰好是这种情况,但不会工作在 Windows 上,但 Windows 确实有 64 位类型和有效的乘法] - 它依赖于具有大于 int 的类型,或者它没有意义。
  • 如果你需要 64 位类型 int64_t 有什么问题
【解决方案2】:

特别是a 乘以b 的数学上正确的方法来检测它是否会溢出是计算两个值的log₂。如果它们的总和高于结果的最高可表示值的 log2,则存在溢出。

log₂(a) + log₂(b) < log₂(UINT_MAX)

难点在于快速计算整数的 log2。为此,可以使用几种位旋转技巧,例如计数位,计数前导零(某些处理器甚至有相关指令)。这个网站有几个实现 https://graphics.stanford.edu/~seander/bithacks.html#IntegerLogObvious

最简单的实现可能是:

unsigned int log2(unsigned int v)
{
  unsigned int r = 0;

  while (v >>= 1) 
    r++;
  return r;
}

在你的程序中你只需要检查then

 if(log2(a) + log2(b) < MYLOG2UINTMAX)
   return a*b;
 else
   printf("Overflow");

签名的情况类似,但必须专门处理否定的情况。

编辑:我的解决方案不完整,并且有一个错误,使测试比必要的更严重。如果 log₂ 函数返回浮点值,则该等式实际上有效。在实现中,我将值限制为无符号整数。这意味着完全有效的乘法被拒绝。为什么?因为log2(UINT_MAX) 被截断 log₂(UINT_MAX)=log₂(4294967295)≈31.9999999997 截断为 31。

我们可以更改实现以替换要比较的常量

#define MYLOG2UINTMAX (CHAR_BIT*sizeof (unsigned int))

【讨论】:

  • 这是一个很好的答案,是的。但是每次乘法计算 2 个数字的对数会被调用,尤其是在循环中意味着性能急剧下降,因为我无法利用缓存
  • 是的,但是在此处介绍的任何解决方案中,您都会获得相同顺序的性能命中。 hacks 的答案需要非常昂贵的除法。
  • @Cool_Coder 您不需要验证每个号码,只需验证用户提供的不受信任的号码。
  • 此外,如果要检查用户输入,您可以忘记性能成本。用户输入时间慢了几个数量级。此处显示的原始 log2 不会花费太多时间,据我估计,它最多为 100 个周期。这意味着您可以在过去 15 年的任何 PC 上每秒计算 ~10 000 000 到 ~50 000 000 条日志。
【解决方案3】:

你可以试试这个:

if ( b > ULONG_MAX / a )        // Need to check a  != 0 before this division  
   return 0;                    //a*b invoke UB
else
   return a*b; 

【讨论】:

  • 但是重点是UB已经发生了,不能依赖这段代码的行为。
  • 这仅在某些处理器上是安全的,并且依赖于理解编译器生成的代码以及是否溢出陷阱。但是,是的,通过相关(生产代码)中的相关测试,它应该可以工作。
  • @OliCharlesworth;是的。你说的对。删除了。
  • 我无法理解这是如何工作的。你能用一两行解释逻辑吗?
  • 为方便起见,采取小幅限制;假设最大限制为255a = 15b = 15,然后是 a*b = 225。如果 b = 10a = 25 则 `b > max_limit / a` 将变为 false,因为 max_limit / a 将变为
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-04
  • 1970-01-01
  • 2021-06-23
相关资源
最近更新 更多