理解释放错误

Understanding a deallocation error

我写了一个小而简单的代码来复制我在另一个更大的代码中遇到的错误:

PROGRAM allocateBug                                                              
    IMPLICIT NONE                                                                  
    INTEGER, PARAMETER :: Nx = 10                                                
    INTEGER, PARAMETER :: Ny = 20                                                
    INTEGER, PARAMETER :: Nz = 30                                                
    REAL, ALLOCATABLE, DIMENSION(:,:,:) :: a                                 

    ALLOCATE(a(0:Nx-1,0:Ny-1,0:Nz-1))                          

    a(Nx+2,:,:) = 0.4                                                            

    PRINT*, "size(a) = ", SIZE(a,1)                                              
    DEALLOCATE(a)  
END PROGRAM allocateBug 

代码的输出是:

`size(a) = 10`

这是以下错误消息:

*** glibc detected *** ./a.out: free(): invalid next size (normal): 0x0000000001a97060 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x7eb96)[0x7f652d0bcb96]
./a.out[0x40719c]
./a.out[0x402ebf]
./a.out[0x402bc6]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xed)[0x7f652d05f76d]
./a.out[0x402ab9]
(... more lines ...)

我在尝试越界访问数组 a 时没有收到错误消息,这是我从 ifort 了解到的功能。为什么只有在释放数组时才会出错?此外,如果我在 NxNx+1 访问 a,代码将无错退出。

编辑

为了澄清我的问题,当打印 a 的大小时,代码告诉我它仍然认为 a 在第一个维度中的大小为 10。但是,解除分配 a 时的错误告诉我,在越界写入时 a 的状态发生了一些变化。我只是很好奇这段代码到底发生了什么,所以才会发生错误。

我认为原因是没有 run-time 检查 reading/writing 数组越界。如果你用 -check bounds 编译,我想它会抱怨,比如

forrtl: severe (408): fort: (2): Subscript #1 of the array A has value 12 which is greater than the upper bound of 9

因此,当它不执行 run-time 检查时,它会愉快地写入该索引应该存在的内存中 - 除了它会覆盖那里的内容。在当前这种情况下,必须有一些东西指定数组本身(记住,在 FORTRAN 中,可分配数组比内存地址 ),并且在释放时,它必须给出一个错误的命令以及在哪里解除分配。

如果您尝试写入操作系统分配给您的可执行文件之外的内存区域,您会遇到段错误,因此这与此类似。

编辑: 您本质上是在询问编译器如何处理可分配数组。例如,在 C 语言中,当你分配时,你所做的就是告诉代码保留一块大小合适的连续内存块,它会告诉你它在哪里,然后你必须跟踪它有多长时间 space 其实是。

在 FORTRAN 中,情况有所不同。当你分配一个变量时,你可以,例如,查询它的长度、形状等。这必须存储在某个地方。它的存储方式和存储位置完全取决于编译器。我不知道它是如何在 ifort 中实现的,但我想每个 allocatable 变量都会有一个 header,即保留的 space,其中与形状相关的所有信息是存储,数组的实际元素随后出现在连续的 space.

当您说 a(Nx+2,:,:) 时,您的代码基于 header 运行,您希望从内存的哪个区域写入 to/read,然后执行。也许在你的情况下,这个操作破坏了 header 本身,当你试图释放变量时,你的代码可能解释它应该释放内存中的 space ,那张可爱的猫照片就是你当前正在浏览。这扰乱了操作系统并告诉你的代码停止。或者它可能解释说它应该释放一个负的内存块并说:什么????

ALLOCATE(a(0:Nx-1,0:Ny-1,0:Nz-1))                          

a(Nx+2,:,:) = 0.4 

在这里,您沿着从 0 到 Nx-1 的第一个维度分配 A。然后你在这些边界之外给它赋值,从 Nx + 2.

坏主意。要么你得到一些奇怪的东西,比如堆损坏,要么你设置了正确的编译器标志并得到运行时错误。 gfortran 抱怨 -fcheck=all that

At line 10 of file a.f90
Fortran runtime error: Index '12' of dimension 1 of array 'a' outside of expected range (0:9)

这足以清楚地表明错误所在。

首先,写越界是一个未定义的操作,因此你的整个程序变得未定义。在这一点上,它所做的任何事情都是正确的。无论您的程序 运行 正常、崩溃、根本不 运行 还是其他一些选项,这都是未定义行为的正确结果,而不是错误。

根据您的评论,您更感兴趣的是糟糕的写入究竟是如何导致无法从低级别的角度解除分配的,而不是仅仅接受未定义的行为可以做任何它想做的事情。

让我们先看看你的数组 a:

a(0:9,0:19,0:29)

大小 (10,20,30),其中包含 6,000 个 4 字节浮点值元素,总共 24,000 字节的存储空间。您未定义的写入是

a(12,:,:) = 0.4

这将写入数组 a(12,0:19,0:29) 的 600 个元素,但只有一个元素超出范围。元素 a(12,19,29) 将写入第 6003 个元素。其他写入将在边界内,但会通过从越界索引写入不正确的元素来破坏数组的内容。

如果您的变量 a 被分配到地址 0x0000-0x5DBF,那么元素 (9,19,29) 将位于地址 0x5DBC-0x5DBF,并且您越界写入元素 (12,19, 29) 将位于 0x5DC8-0x5DCB,或超出数组末尾的 8-12 个字节。

接下来的内容取决于实现,并且基于对 gfortran 4.9.2 的分析。

与 C 不同,Fortran 中的数组具有称为 "array descriptors" 的元数据。 GNU gfortran 对 4 字节实数数组使用以下描述符:

 typedef struct gfc_array_r4 { 
   GFC_REAL_4 *base_addr;
   size_t offset;
   index_type dtype;
   descriptor_dimension dim[r];
 }

变量descriptor_dimension是一个长度为GFC_MAX_DIMENSIONS的数组,结构如下:

 typedef struct descriptor_dimension
 {
   index_type _stride;
   index_type lower_bound;
   index_type _ubound;
 }

您的示例代码仍然可以告诉您 a 的正确大小的原因是此元数据包含该信息。

遵循可分配组件的内部代码路径更加困难,我没有时间进行适当的检查。然而,粗略地看,似乎有更多与可分配类型和各种分配策略(malloc 和其他)相关的元数据。


我从上面可以做出的唯一一般性陈述是,gcc 内部的解除分配例程工作所需的一些重要数据位于内存中,至少部分位于内存中超出末尾的 8-12 个字节阵列内存。当您写入数组末尾后 0 到 8 个字节之间的内存并且没有发现致命的 运行 时间错误时,您没有覆盖重要数据。您正在破坏的数据的细节以及它在堆中相对于您的数组的排列方式在很大程度上取决于实现,不仅在编译器供应商之间,而且可能在编译器版本之间。

此外,请注意,当写入像 a(12,0,0) 这样的数组元素时,相对于分配的数组内存而言是在边界内,但相对于您的维度边界而言是边界外的。虽然它不会在没有边界检查的情况下发出 运行time 错误,但请注意,例如a(12,0,0) 与内存中的 a(2,1,0) 是相同的元素,因此您的越界写入破坏了越界值。