Ada 中的无约束数组可以安全使用吗?

Are unconstrained arrays in Ada safe to use?

我正在阅读 Ada 中的数组类型,发现有趣的是,与 C++ 不同,该语言允许它们的大小在编译时未知。我不确定它们是如何实现的,所以我写了一个小测试:

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Command_Line; use Ada.Command_Line;

procedure Main is
   type Data is array (1 .. 131_072) of Integer;
   type Vector is array (Positive range <>) of Data;
   Sequence : Vector (1 .. Argument_Count);
begin
   for I in Sequence'Range loop
      Sequence (I) := (others => I);
      Put (Integer'Image (Sequence (I)(1)));
   end loop;
end Main;

然后尝试使用可变长度数组在 C 中复制此代码:

#include <stdio.h>

struct data {
    int x[131072];
};

int main(int argc, char** argv) {
    (void)argv;
    struct data sequence[argc - 1];
    for (int i = 0; i < argc - 1; i++) {
        for (int j = 0; j < 131072; j++)
            sequence[i].x[j] = i;
        printf("%i ", sequence[i].x[1]);
    }
}

我用 gnatmake -gnato -fstack-check -gnat2012 -gnata -O3 main.adb -o maingcc -O3 -Wall -Werror -Wextra -pedantic cmain.c -o cmain 编译。在 运行 程序之后,当给定 16 个或更多参数时,两者都失败了 - 区别在于 cmain 只是段错误,而 main 最终引发“STORAGE_ERROR:堆栈溢出或错误内存访问。

由于 VLA 和无约束阵列似乎(至少在表面上)以类似的方式实现,并且前者被广泛认为几乎在所有情况下都不能安全使用,那么使用后者安全吗?

无约束数组本质上并非不安全。请参阅下面实现数组元素并行添加的示例。

package Parallel_Addition is
   type Data_Array is array(Integer range <>) of Integer;
   type Data_Access is access all Data_Array;
   function Sum(Item : in not null Data_Access) return Integer;
end Parallel_Addition;

请注意,上面的包规范声明了一个不受约束的数组类型。它还声明了对无约束数组类型的访问类型,以便可以动态分配非常大的数组,避免堆栈耗尽问题。

package body Parallel_Addition is

   ---------
   -- Sum --
   ---------

   function Sum (Item : in not null Data_Access) return Integer is
      task type Adder is
         entry Set (Min : Integer; Max : Integer);
         entry Report (Value : out Integer);
      end Adder;

      task body Adder is
         Total : Integer := 0;
         First : Integer;
         Last  : Integer;
      begin
         accept Set (Min : Integer; Max : Integer) do
            First := Min;
            Last  := Max;
         end Set;
         for I in First .. Last loop
            Total := Total + Item (I);
         end loop;
         accept Report (Value : out Integer) do
            Value := Total;
         end Report;
      end Adder;
      A1  : Adder;
      A2  : Adder;
      R1  : Integer;
      R2  : Integer;
      Mid : constant Integer := (Item'Length / 2) + Item'First;
   begin
      A1.Set (Min => Item'First, Max => Mid);
      A2.Set (Min => Mid + 1, Max => Item'Last);
      A1.Report (R1);
      A2.Report (R2);
      return R1 + R2;
   end Sum;

end Parallel_Addition;

在函数 Sum 中使用无约束数组实例没有问题。 让我们尝试使用一个非常大的动态分配数组来测试这个包。

with Parallel_Addition; use Parallel_Addition;
with Ada.Text_IO;       use Ada.Text_IO;
with Ada.Calendar;      use Ada.Calendar;

procedure Parallel_Addition_Test is
   The_Data : Data_Access := new Data_Array (1 .. Integer'Last);
   Start    : Time;
   Stop     : Time;
   The_Sum  : Integer;

begin
   The_Data.all := (others => 1);
   Start        := Clock;
   The_Sum      := Sum (The_Data);
   Stop         := Clock;
   Put_Line ("The sum is: " & Integer'Image (The_Sum));
   Put_Line
     ("Addition elapsed time is " &
      Duration'Image (Stop - Start) &
        " seconds.");
   Put_Line
     ("Time per addition operation is " &
        Float'Image(Float(Stop - Start) / Float(The_Data'Length)) &
        " seconds.");
end Parallel_Addition_Test;

在我的 Windows 10 PC 上执行时,我得到以下输出:

The sum is:  2147483647
Addition elapsed time is  5.141288000 seconds.
Time per addition operation is  2.39410E-09 seconds.

Ada 中不受约束的数组类型本身并不安全;使用未经检查的输入值(例如 Argument_Count)来创建该大小的数组对象是不安全的,或者至少可能导致异常。如果你这样做,并且没有异常处理程序,攻击者可以使你的程序中止并出现未处理的异常,如你的示例所示。

请注意,在 Ada 中以两种方式使用不受约束的数组类型:

  1. 创建动态确定大小的数组对象,如您的示例所示。
  2. 将各种大小的数组作为参数传递给子程序,即使实际的数组对象具有静态定义的大小。

第二次使用(参数)当然是完全安全的,参数的实际边界可以像往常一样通过A'First、A'Last、A'Range、A'Length访问。