ifort:一切正常,但使用 gfortran:分段错误

计算科学 正则
2021-12-11 18:49:10

在下面的代码中,如果我使用 ifort 编译运行正常,但如果我使用 gfortran 运行它会出现错误:“程序收到信号 SIGSEGV:分段错误 - 内存引用无效。”

有人可以告诉我我做错了什么吗?或者也许是 gfortran 的问题。

PROGRAM testing
IMPLICIT NONE
REAL    ::  coordN(3,2),aux(2,2),vec(2)
INTEGER ::  cara(3,2),c
!
coordN = 0.0;  cara = 0;  vec = 0.0
!
coordN(1,1)=-1.0;   coordN(1,2) = -2.0
coordN(2,1)=-10.0;  coordN(2,2) = -20.0
coordN(3,1)=-100.0; coordN(3,2) = -200.0
!
cara(1,1) = 1;  cara(1,2) = 2
cara(2,1) = 1;  cara(2,2) = 3
cara(3,1) = 3;  cara(3,2) = 2
!
PRINT*,SIZE(coordN(cara(c,:),:),1),SIZE(coordN(cara(c,:),:),2)
aux = coordN(cara(c,:),:)
!
END program testing

注意

    PRINT*,SIZE(coordN(cara(c,:),:),1),SIZE(coordN(cara(c,:),:),2)

说 coordN(cara(c,:),:) 实际上是一个 2x2 实矩阵。所以,我不明白为什么我做不到

aux = coordN(cara(c,:),:)
1个回答

该变量c没有在程序的任何地方初始化,因此它在运行时的值可以是任何值。在这里,我对上面显示的程序进行了轻微修改:

PROGRAM testing
IMPLICIT NONE
REAL    ::  coordN(3,2),aux(2,2),vec(2)
INTEGER ::  cara(3,2),c
!
coordN = 0.0;  cara = 0;  vec = 0.0
!
coordN(1,1)=-1.0;   coordN(1,2) = -2.0
coordN(2,1)=-10.0;  coordN(2,2) = -20.0
coordN(3,1)=-100.0; coordN(3,2) = -200.0
!
cara(1,1) = 1;  cara(1,2) = 2
cara(2,1) = 1;  cara(2,2) = 3
cara(3,1) = 3;  cara(3,2) = 2
!
PRINT *, c
PRINT *, SIZE(coordN(cara(c,:),:),1),SIZE(coordN(cara(c,:),:),2)
aux = coordN(cara(c,:),:)
!
END program testing

关键部分是这个程序打印出cfirst 的值。当我在我的机器上运行这个程序时,我每次都会得到不同的值。一次运行,我得到-2147483648;在另一个上,我得到 0。在第三个上,我得到 0,但是 - 我们将在几秒钟内得到这个 - gfortran 运行时库给了我这个错误:runtime error: load of address 0x7ffc33ea35d8 with insufficient space for an object of type 'real(kind=4)'.

发生这一切的原因是,如果你不初始化一个变量,它在运行时的值可能是完整的,完全是垃圾。 这种垃圾的确切性质是平台相关的。该值可以是最后一个访问该 RAM 区域的进程留在内存中的任何值。或者,沿着这条线的某个地方,所有内存都可能被初始化为零,或者由您的操作系统作为安全措施,或者由 fortran 运行时库。如果c恰好为零,那么一种可能的行为是读取该数组开始之前的内存地址。该地址仍然超出数组边界,但它仍然可能位于程序堆栈或堆中的某个位置。但正如我上面提到的,它也可能是一个完全荒谬的值,它超过了 Avogadro 的数字,更不用说你机器的 RAM 大小了。

这是我一直犯的错误。 幸运的是,这种事情有一些技巧,这里有一些我最喜欢的:

  1. 用 编译-fbounds-check您可以在 Fortran 中执行此操作,因为该语言保证您可以在运行时检查所有数组的大小。这个编译器标志在 C 中是不可能的,因为“数组”只是内存的连续区域,而你,程序员,必须自己跟踪大小。
  2. 编译所有警告:-Wall -Wextra -Wpedantic. 你可能会得到很多误报,但我总是在我第一次编写的任何代码中发现奇怪的、狡猾的东西。
  3. 使用调试标志编译且不进行优化 ( -g -O0) 并在可执行文件上使用 valgrind: valgrind ./testValgrind 通常可以查明代码中出现问题的确切行并为您提供堆栈跟踪。这可能不是你所期望的!
  4. 使用 sanitizers 编译:-fsanitize=address, 或-fsanitize=undefined. 消毒剂可以查明内存泄漏,如果您用 C 编写代码,这将是真正的救命稻草。可以在 valgrind 失败的地方工作,也可以在 valgrind 工作的地方失败。我在一次执行时收到的错误消息c = 0是使用地址清理程序时得到的。
  5. 如果您遇到一个奇怪的 heisenbug 并没有出现在每个程序调用中,您可以使用rr确定性地记录几个程序跟踪,然后您可以稍后使用 GDB 手动单步执行。

我真的很讨厌编程语言的教条主义,但这里有一些值得考虑的因素。Fortran 在数组大小方面提供了比 C 更强大的运行时保证,因此类似的技巧-fbounds-check是可能的并且非常有用。另一方面,C 的工具Fortran 的工具要好得多,因为它的使用范围更广,而且故障具有安全隐患。例如,当我试图找出哪些编译器标志会使这个问题变得非常明显时,我的第一个想法是:啊哈,我将-Wall使用未初始化的值进行编译,我会收到警告。但我惊讶地发现 gfortran 根本没有发出警告,唯一可靠的标志是-fbounds-check. 如果我编写了一个等效的 C 程序,gcc 会给出未定义行为的警告。这并不是说您应该使用 C 而不是 Fortran,反之亦然——您很少有选择的奢侈。但是存在差异,您应该意识到它们。