由于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
...
movl -232(%rax,%rdx,4), %eax
movl %eax, baz_+4(%rip)
第一步准备函数调用的参数(在我的示例中为call boo(bar)
,其中boo
具有声明为使用假定形状数组的接口)。它将bar
的数组描述符地址移动到EDI
中。这是一个32位立即移动,因此预期描述符在前2 GiB内。实际上,在small
和medium
内存模型中,它被分配在.bss
中:
.local bar.1580
.comm bar.1580,72,32
bar
被描述符传递到另一个子程序时发生的解释。 - Hristo Iliev