【问题标题】:Why are global variables in x86-64 accessed relative to the instruction pointer?为什么 x86-64 中的全局变量相对于指令指针被访问?
【发布时间】:2019-10-09 07:12:22
【问题描述】:

我尝试使用 gcc -S -fasm foo.c 将 c 代码编译为汇编代码。 c代码在main函数中声明全局变量和变量如下图:

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:

【问题讨论】:

  • 仅仅因为它是相对于rip 访问的,并不意味着它在.text 段(而是部分)中。正如您在汇编代码中清楚地看到的那样,它前面有一个.data,所以它不在.text 中。它只是一种与位置无关的内存寻址方式,现在由于 ASLR,它经常默认启用。

标签: c assembly compiler-construction x86-64


【解决方案1】:

虽然 .data 和 .text 段彼此独立,但一旦链接,它们相对于彼此的偏移量是固定的(至少在 gcc x86-64 -mcmodel=small 代码模型中,这是默认代码模型和适用于所有代码+数据小于 2GB 的程序)。

因此,无论系统在进程的地址空间中加载可执行文件的何处,它们引用的指令和数据都将具有相对于彼此的固定偏移量。

由于这些原因,为(默认)小型代码模型编译的 x86-64 程序对代码和全局数据都使用 RIP 相对寻址。这样做意味着编译器不需要指定一个寄存器来指向系统加载可执行文件的 .data 部分的位置;程序已经知道它自己的 RIP 值以及它与要访问的全局数据之间的偏移量,因此访问它的最有效方法是通过 RIP 的 32 位固定偏移量。

(绝对 32 位寻址模式会占用更多空间,而 64 位绝对寻址模式效率更低,并且仅适用于 RAX/EAX/AX/AL。)

您可以在 Eli Bendersky 的网站上找到更多信息:Understanding the x64 code models

【讨论】:

  • 现代 Linux 发行版使 gcc 默认使用小型 PIC 代码模型,用于 PIE 可执行文件。 x64 是 Windows 术语,一般不用于 Linux 或 GCC。否则,很好的答案。
  • “小”代码模式是位置相关的,所以所有地址都是链接时间常数。您关于需要指向 .data 部分的指针的观点仅适用于小型 PIC/PIE 模型,其中在链接时不知道绝对静态地址。
【解决方案2】:

可执行文件不同部分之间的偏移量是链接时常量,因此 RIP 相对寻址可用于任何部分(包括非const 全局变量所在的.data)。请注意 asm 输出中的 .data。

这甚至适用于 PIE 可执行文件或共享库,其中绝对地址不直到运行时 (ASLR) 才知道。

位置无关可执行文件 (PIE) 的运行时 ASLR 会为整个程序随机分配一个基地址,而不是相对于彼此的各个段起始地址。

对静态变量的所有访问都使用 RIP 相对寻址,因为这是最有效的,即使在可以选择绝对寻址的位置相关可执行文件中(因为静态代码/数据的绝对地址是链接-时间常数,不通过动态链接重定位)。


相关并且可能重复:


在 32 位 x86 中,有两种冗余方式来编码无寄存器的寻址模式和 disp32 绝对地址。 (有和没有 SIB 字节)。 x86-64 将较短的一个重新用作RIP+rel32,因此mov foo, %eax 比mov 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-bit absolute addresses no longer allowed in x86-64 Linux?,它对所有内容都使用与位置无关的代码,包括 RIP 相对 LEA,例如 7 字节 lea .LC0(%rip), %rdi 而不是 5 字节 mov $.LC0, %edi。见How to load address of function or label into register

我之所以提到 Linux,是因为它从 .cfi 指令看起来就像您正在为非 Windows 平台编译一样。

【讨论】:

    猜你喜欢
    • 2017-03-12
    • 1970-01-01
    • 2019-02-19
    • 2014-05-18
    • 2013-12-06
    • 2021-11-11
    • 1970-01-01
    • 2016-11-27
    • 1970-01-01
    相关资源
    最近更新 更多