gfortran for dummy:mcmodel=medium 到底做了啥?

Posted

技术标签:

【中文标题】gfortran for dummy:mcmodel=medium 到底做了啥?【英文标题】:gfortran for dummies: What does mcmodel=medium do exactly?gfortran for dummy:mcmodel=medium 到底做了什么? 【发布时间】:2012-10-06 15:21:45 【问题描述】:

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

  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 数组,一个没有)或多或少是等价的......

【问题讨论】:

【参考方案1】:

由于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 位(有符号)偏移量来寻址EAX 的值必须移入其中的b 变量。这仅在链接时检测到。汇编器本身不知道适当的偏移量,因为它不知道指令指针 (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位值放入寄存器R10bar.1535 符号的内存是使用 .largecomm 指令分配的,因此它在 ELF 可执行文件的 .lbss 部分结束。 .lbss 用于存储可能不适合前 2 GiB 的符号(因此不应使用 32 位指令或 RIP 相对寻址来寻址),而较小的东西转到 .bssbaz_ 仍然使用.comm 而不是.largecomm 分配)。由于在 ELF 链接描述文件中 .lbss 部分位于 .bss 部分之后,因此 baz_ 不会最终无法使用 32 位 RIP 相关寻址。

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

当改用 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 传递给它,则bar 的描述符被创建为自动变量(即在foo 的堆栈上)。如果使用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

【讨论】:

这是一个很好的解释。谢谢。这给了我一个很好的开始,让我更深入地研究这些东西(这就是我一直在寻找的东西)。 @mgilson,为了回答的完整性,我还添加了对 bar 通过描述符传递给另一个子例程时发生的情况的解释。【参考方案2】:

不,如果您不使用-mcmodel=medium,大型静态数组(如您的bar)可能会超出限制。但是当然可以分配更好。对于可分配对象,只有数组描述符必须适合 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.

【讨论】:

我想,也许问题是“静态数组”和“可分配数组”之间有什么区别?我的印象是,在这两种情况下,它们都是从堆中分配的(尽管我应该承认我说的是我不太了解的东西) 你写的时候我刚刚编辑了答案。可分配对象有一个描述符(带有附加数据的指针),只有这个必须适合 2 GB。静态数组完全在静态段中,就像任何其他静态变量一样。 (可能静态段中只有一个指向描述符的指针,但并没有改变区别。) 如果我理解正确,静态数组的 2GB 限制不再适用于 mcmodel=small。这是正确的吗? 我认为它确实适用,它不适用于中型和大型。

以上是关于gfortran for dummy:mcmodel=medium 到底做了啥?的主要内容,如果未能解决你的问题,请参考以下文章

SLAM初探-SLAM for Dummies

SLAM for dummies中文翻译

什么是循环冗余检查以及它是如何简单地工作的(for-dummies 风格)?

链资料Blockchain For Dummies电子书

如何在 pyqt 中嵌入 matplotlib - For Dummies

如何在 pyqt 中嵌入 matplotlib - For Dummies