处理表达式时如何检查 Ada 中的潜在溢出?

How to check for potential overflow in Ada when dealing with expression?

我对 Ada 比较陌生,一直在使用 Ada 2005。但是,我觉得这个问题与所有语言都相关。

我目前正在使用 Codepeer 等静态分析工具来解决我代码中的潜在漏洞。

我正在讨论的一个问题是如何在分配可能导致变量溢出的表达式之前处理检查。

这可以用一个例子更好地解释。假设我有一个无符号 32 位整数类型的变量。我正在为这个变量 CheckMeForOverflow:

分配一个表达式
CheckMeForOverflow := (Val1 + Val2) * Val3;

我的困境是如何在这种情况下有效地检查溢出 - 这似乎经常出现在代码中。是的,我可以这样做:

if ((Val1 + Val2) * Val3) < Unsigned_Int'Size then
    CheckMeForOverflow := (Val1 + Val2) * Val3;
end if;

我的问题是,如果没有溢出的可能性,检查表达式然后立即分配相同的表达式似乎效率低下。

不过,我上网查了一下,这好像挺常见的。谁能解释更好的选择或解释为什么这是一个不错的选择?我不希望它分散在我的代码中。

我也意识到我可以创建另一个更大类型的变量来保存表达式,对新变量进行评估,然后将该变量的值分配给 CheckMeForOverflow,但话又说回来,这将意味着创建一个新变量并仅使用它来执行一次检查,然后再也不会使用它。这看起来很浪费。

有人可以提供一些见解吗?

非常感谢!

我个人会这样做

begin
   CheckMeForOverflow := (Val1 + Val2) * Val3;
exception
   when constraint_error =>
                 null; --  or log that it overflowed
end;

但请注意,您的变量不能有可用的值。

它比 if 结构更清晰,我们不会执行两次计算。

这正是SPARK可以帮助解决的问题。它允许您证明您不会 运行 给定有关计算输入的某些假设的时间错误。

如果您从这个包中的 No_Overflow 这样的简单函数开始:

with Interfaces; use Interfaces;

package Show_Runtime_Errors is

   type Unsigned_Int is range 0 .. 2**32 - 1;
   function No_Overflow (Val1, Val2, Val3 : Unsigned_Int) return Unsigned_Int;

end Show_Runtime_Errors;


package body Show_Runtime_Errors is

   function No_Overflow (Val1, Val2, Val3 : Unsigned_Int) return Unsigned_Int is
      Result : constant Unsigned_Int := (Val1 + Val2) * Val3;
   begin
      return Result;
   end No_Overflow;

end Show_Runtime_Errors;

然后当你 运行 SPARK 时,你会得到以下内容:

Proving...
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
show_runtime_errors.adb:4:55: medium: range check might fail (e.g. when Result = 10)
show_runtime_errors.adb:4:55: medium: overflow check might fail (e.g. when
   Result = 9223372039002259450 and Val1 = 4 and Val2 = 2147483646 and
   Val3 = 4294967293)
gnatprove: unproved check messages considered as errors
exit status: 1

现在,如果您像这样向 No_Overflow 添加一个简单的前提条件:

function No_Overflow (Val1, Val2, Val3 : Unsigned_Int) return Unsigned_Int with
   Pre => Val1 < 2**15 and Val2 < 2**15 and Val3 < 2**16;

然后 SPARK 生成以下内容:

Proving...
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
Success!

输入范围的实际先决条件显然取决于您的应用。

备选方案是您假设的解决方案,在计算表达式之前在代码中放置大量显式保护,或者通过异常处理捕获 运行time 错误。 SPARK 相对于这些方法的优势在于,如果您可以提前证明不会出现 运行 时间错误,则无需使用 运行 时间检查来构建软件。

请注意,先决条件是 Ada 2012 的一项功能。您还可以在整个代码中使用 pragma Assert,SPARK 可以利用它来进行证明。

有关 SPARK 的更多信息,请参见此处的教程: https://learn.adacore.com/courses/intro-to-spark/index.html

要亲自尝试,您可以将上面的代码粘贴到此处的示例中: https://learn.adacore.com/courses/intro-to-spark/book/03_Proof_Of_Program_Integrity.html#runtime-errors

顺便说一下,您建议的代码:

if ((Val1 + Val2) * Val3) < Unsigned_Int'Size then
    CheckMeForOverflow := (Val1 + Val2) * Val3;
end if;

由于两个原因而无法工作:

  1. Unsigned_Int'Size是表示Unsigned_Int所需的位数。您可能想要 Unsigned_Int'Last
  2. ((Val1 + Val2) * Val3) 甚至可以在与 Unsigned_Int'Last 的比较完成之前溢出。因此,此时您将生成一个异常,并在异常处理程序中崩溃或处理它。