gfortran入门指南:mcmodel=medium具体是做什么的?

19

我有一些代码在编译时会给我提供重定位错误,下面是一个说明问题的示例:

  program main
  common/baz/a,b,c
  real a,b,c
  b = 0.0
  call foo()
  print*, b
  end

  subroutine foo()
  common/baz/a,b,c
  real a,b,c

  integer, parameter :: nx = 450
  integer, parameter :: ny = 144
  integer, parameter :: nz = 144
  integer, parameter :: nf = 23*3
  real :: bar(nf,nx*ny*nz)

  !real, allocatable,dimension(:,:) :: bar
  !allocate(bar(nf,nx*ny*nz))

  bar = 1.0
  b = bar(12,32*138*42)

  return
  end

使用 gfortran -O3 -g -o test test.f 编译时,我遇到了以下错误:

relocation truncated to fit: R_X86_64_PC32 against symbol `baz_' defined in COMMON section in /tmp/ccIkj6tt.o

如果我使用 gfortran -O3 -mcmodel=medium -g -o test test.f 就可以工作。还要注意,如果我让数组可分配并在子例程中分配它,则也可以正常工作。

我的问题是,-mcmodel=medium 到底是做什么的?我曾经认为,有 allocatable 数组和没有这种数组的两个版本的代码更或多或少是等价的......

2个回答

34

由于bar很大,编译器会生成静态分配而不是在栈上自动分配。 静态数组使用.comm汇编指令创建,在所谓的COMMON部分中创建一个分配。 来自该部分的符号被收集,同名符号被合并(缩减为一个请求大小等于所请求的最大大小的符号),然后剩下的内容映射到大多数可执行文件格式中的BSS(未初始化数据)部分。 对于ELF可执行文件,.bss部分位于数据段中,位于堆的数据段部分之前(还有另一部分堆由匿名内存映射管理,不驻留在数据段中)。

使用small内存模型时,使用32位寻址指令来寻址x86_64上的符号。 这使代码更小且更快。 使用small内存模型时的一些汇编输出:

movl    $bar.1535, %ebx    <---- Instruction length saving
...
movl    %eax, baz_+4(%rip) <---- Problem!!
...
.local  bar.1535
.comm   bar.1535,2575411200,32
...
.comm   baz_,12,16

这段代码使用了一个32位的移动指令(长度为5字节),将bar.1535符号的值(该值等于符号位置的地址)放入RBX寄存器的低32位中(高32位清零)。bar.1535本身是使用.comm指令分配的。在这之后为baz COMMON块分配内存。由于bar.1535非常大,所以baz_最终距离.bss段的开头超过了2 GiB。这导致在第二个movl指令中出现问题,因为需要使用从RIP开始的一个非32位(有符号)偏移来寻址b变量,将EAX的值移入其中。这只能在链接时检测到。汇编器本身不知道适当的偏移量,因为它不知道指令指针(RIP)的值会是什么(它取决于代码加载的绝对虚拟地址,由链接器确定),所以它只能把一个偏移量0放进去,然后创建一个类型为R_X86_64_PC32的重定位请求。它指示链接器使用实际偏移值来修补0的值。但它无法这样做,因为偏移值不适合于有符号32位整数,因此退出。

有了medium内存模型,情况就像这样:

movabsq $bar.1535, %r10
...
movl    %eax, baz_+4(%rip)
...
.local  bar.1535
.largecomm      bar.1535,2575411200,32
...
.comm   baz_,12,16
首先使用一个64位立即移动指令(长度为10个字节),将表示地址为 bar.1535 的64位值放入寄存器R10中。使用.largecomm指令分配了bar.1535符号的内存,因此其最终位于ELF可执行文件的.lbss部分。.lbss用于存储可能不适合在前2 GiB(因此不应使用32位指令或RIP相对寻址访问)中的符号,而较小的内容则转到.bss中(baz_仍然使用.comm而不是.largecomm进行分配)。由于在ELF链接器脚本中.lbss 部分位于.bss 部分之后,因此不会使用32位RIP相关寻址无法访问baz_

所有寻址模式都在System V ABI: AMD64 Architecture Processor Supplement中描述。这是一篇很重的技术文章,但任何真正想了解大多数x86_64 Unix上64位代码如何工作的人都必须阅读。

当使用ALLOCATABLE数组时,gfortran 会分配堆内存(最可能实现为匿名内存映射,考虑到分配的大容量):

movl    $2575411200, %edi
...
call    malloc
movq    %rax, %rdi

这基本上就是RDI = malloc(2575411200)。从那时起,bar的元素可以通过使用从RDI存储的值开始的正偏移来访问:

movl    51190040(%rdi), %eax
movl    %eax, baz_+4(%rip)

如果相对于 bar 的起始位置超过2 GiB,则会采用更复杂的方法。例如,为了实现 b = bar(12,144*144*450)gfortran 会发出以下指令:

; Some computations that leave the offset in RAX
movl    (%rdi,%rax), %eax
movl    %eax, baz_+4(%rip)

这段代码不受内存模型的影响,因为没有假设动态分配的地址。同时,由于数组未被传递,因此不会构建描述符。如果添加另一个函数,该函数需要使用一个假定形状的数组,并将 bar 传递给它,则会在自动变量(即 foo 的堆栈)中创建 bar 的描述符。如果使用 SAVE 属性使数组变为静态,则描述符将放置在 .bss 部分:

movl    $bar.1580, %edi
...
; RAX still holds the address of the allocated memory as returned by malloc
; Computations, computations
movl    -232(%rax,%rdx,4), %eax
movl    %eax, baz_+4(%rip)

第一步准备函数调用的参数(在我的示例中为call boo(bar),其中boo具有声明为使用假定形状数组的接口)。它将bar的数组描述符地址移动到EDI中。这是一个32位立即移动,因此预期描述符在前2 GiB内。实际上,在smallmedium内存模型中,它被分配在.bss中:

.local  bar.1580
.comm   bar.1580,72,32

2
这是一个非常好的解释。谢谢。这为我提供了一个很好的开始,让我更深入地研究这些东西(这正是我想要的)。 - mgilson
@mgilson,为了完整回答,我已经添加了bar被描述符传递到另一个子程序时发生的解释。 - Hristo Iliev

10

不,像你的bar这样的大型静态数组可能会超过限制,如果你不使用-mcmodel=medium。但是allocatables当然更好。对于allocatables,只有数组描述符必须适合2 GB,而不是整个数组。

来自GCC参考:

-mcmodel=small
Generate code for the small code model: the program and its symbols must be linked in the lower 2 GB of the address space. Pointers are 64 bits. Programs can be statically or dynamically linked. This is the default code model. 
-mcmodel=kernel
Generate code for the kernel code model. The kernel runs in the negative 2 GB of the address space. This model has to be used for Linux kernel code. 
-mcmodel=medium
Generate code for the medium model: The program is linked in the lower 2 GB of the address space but symbols can be located anywhere in the address space. Programs can be statically or dynamically linked, but building of shared libraries are not supported with the medium model. 
-mcmodel=large
Generate code for the large model: This model makes no assumptions about addresses and sizes of sections. Currently GCC does not implement this model.

1
我猜,也许问题是“静态数组”和“可分配数组”的区别在哪里?我的印象是,在这两种情况下它们都将从堆中分配(尽管我必须承认我正在谈论我不太了解的事情)。 - mgilson
如果我理解正确的话,对于 mcmodel=small,静态数组的2GB限制不再适用。这是正确的吗? - Z boson
2
我认为它适用,但不适用于中大型。 - Vladimir F Героям слава
@NealTitusThomas,我不知道这个错误信息。我建议您提出一个新问题。 - Vladimir F Героям слава
@VladimirF 很不幸,我的问题配额已经用完了。我发现我的数组大小太大了,解决这个问题的大多数方法是使用 -mcmodel=medium/large 或使用可分配数组。我尝试了前者,但对后者不确定,因为我很新于Fortran,而我正在处理的代码是一些旧的遗留代码。 - Neal Titus Thomas
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接