为什么x86-64架构中的全局变量是相对于指令指针访问的?

12

我曾尝试使用gcc -S -fasm foo.c将c代码编译为汇编代码。

该c代码声明了全局变量和主函数中的变量,如下所示:

int y=6;
int main()
{
        int x=4;
        x=x+y;
        return 0;
}

现在我查看了从这段C代码生成的汇编代码,并且我注意到全局变量y使用rip指令指针的值来存储。

我曾以为只有const全局变量存储在文本段中,但是看了这个例子后,似乎普通的全局变量也存储在文本段中,这非常奇怪。

我猜想我做出了一些错误的假设,所以能否有人解释一下呢?

C编译器生成的汇编代码:

        .file   "foo.c"
        .text
        .globl  y
        .data
        .align 4
        .type   y, @object
        .size   y, 4
y:
        .long   6
        .text
        .globl  main
        .type   main, @function

main:
.LFB0:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        movl    $4, -4(%rbp)
        movl    y(%rip), %eax
        addl    %eax, -4(%rbp)
        movl    $0, %eax
        popq    %rbp
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:

4
虽然它是相对于 rip 访问的,但这并不意味着它在 .text 段(或部分)中。正如您在汇编代码中清楚地看到的那样,它前面有一个 .data,因此它不在 .text 中,而只是一种独立于位置的访问内存的方式。由于地址空间布局随机化 (ASLR) 的普及,现在经常默认启用它。 - Jester
2个回答

20
不同段之间的偏移是连接时间常量,因此RIP相对寻址适用于任何段(包括非const全局变量所在的.data段)。请注意您的汇编输出中的.data。 即使在PIE可执行文件或共享库中,这也适用,其中绝对地址直到运行时才能确定(ASLR)。 由于静态代码/数据的绝对地址是连接时间常量,在这种情况下不会被动态链接重定位,因此所有对静态变量的访问都使用RIP相对寻址是最有效的,即使在可以使用绝对寻址的位置相关可执行文件中也是如此。

相关且可能重复:


在32位x86上,有两种冗余的方式可对一个不带寄存器但带disp32绝对地址的寻址模式进行编码(有和没有SIB字节)。x86-64将较短的一种重新定义为RIP+rel32,因此mov foo, %eaxmov foo(%rip), %eax多1个字节。

64位绝对寻址需要更多空间,并且仅适用于mov到/从RAX/EAX/AX/AL,除非您首先使用另一个指令将地址加载到寄存器中。

(在x86-64 Linux PIE/PIC中,允许使用64位绝对寻址,并通过加载时修复来处理,以将正确的地址放入代码、跳转表或静态初始化的函数指针中。因此,代码技术上不必是位置无关的,但通常最好如此。而32位绝对寻址是不允许的,因为ASLR不仅限于虚拟地址空间的低31位。)


请注意,在非PIE Linux可执行文件中,gcc将使用32位绝对寻址将静态数据的地址放入寄存器中。例如,puts("hello");通常会编译为

mov   $.LC0, %edi     # mov r32, imm32
call  puts
在默认的非PIE(位置无关执行)内存模型中,静态代码和数据链接到虚拟地址空间的低32位,因此32位绝对地址无论是零扩展还是符号扩展到64位都可以工作。 这也很方便用于例如索引静态数组,如mov array(%rax), %edx; add $4, %eax
请参阅有关PIE可执行文件的更多信息,其中包括为所有内容使用位置无关代码的 32位绝对地址在x86-64 Linux上不再允许?,包括RIP相对LEA像7字节lea .LC0(%rip),%rdi而不是5字节的mov $.LC0,%edi。 请参阅如何将函数或标签的地址加载到寄存器 我提到Linux,因为从.cfi指令看起来您正在编译非Windows平台的内容。


7
尽管.data和.text段是彼此独立的,但一旦链接,它们相对于彼此的偏移量是固定的(至少在gcc x86-64 -mcmodel=small代码模型中是如此,默认代码模型适用于所有代码+数据小于2GB的程序)。
因此,无论系统在进程的地址空间中加载可执行文件的位置在哪里,指令和它们引用的数据之间的偏移量都是固定的。
由于这些原因,为了使用RIP相对寻址来访问代码和全局数据,x86-64编译的程序使用(默认)小代码模型。这样做意味着编译器不需要专门分配一个寄存器来指向系统加载可执行文件的.data部分;程序已经知道自己的RIP值以及它想要访问的全局数据与其之间的偏移量,因此访问最有效的方式是通过从RIP偏移32位的固定偏移量进行访问。
(绝对32位寻址模式将占用更多空间,64位绝对寻址模式效率更低,并且仅适用于RAX/EAX/AX/AL。)
您可以在Eli Bendersky的网站上找到更多信息:了解x64代码模型

1
现代Linux发行版将gcc默认设置为小的PIC代码模型,用于PIE可执行文件。x64是Windows术语,在Linux或GCC中通常不使用。除此之外,回答得很好。 - Peter Cordes
“小型”代码模式是位置相关的,因此所有地址都是链接时常量。您提到需要指向.data节的指针只适用于小型PIC / PIE模型,在该模型中,绝对静态地址在链接时未知。 - Peter Cordes

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