为什么静态变量的地址相对于指令指针?

Att*_*lio 6 c assembly gcc cpu-registers

我正在按照本教程关于装配.

根据教程(我也在本地尝试,并得到类似的结果),以下源代码:

int natural_generator()
{
        int a = 1;
        static int b = -1;
        b += 1;              /* (1, 2) */
        return a + b;
}
Run Code Online (Sandbox Code Playgroud)

编译到这些汇编指令:

$ gdb static
(gdb) break natural_generator
(gdb) run
(gdb) disassemble
Dump of assembler code for function natural_generator:
push   %rbp
mov    %rsp,%rbp
movl   $0x1,-0x4(%rbp)
mov    0x177(%rip),%eax        # (1)
add    $0x1,%eax
mov    %eax,0x16c(%rip)        # (2)
mov    -0x4(%rbp),%eax
add    0x163(%rip),%eax        # 0x100001018 <natural_generator.b>
pop    %rbp
retq   
End of assembler dump.
Run Code Online (Sandbox Code Playgroud)

(行号的意见(1),(2)(1, 2)通过加我.)

问题:为什么在编译的代码中,静态变量的地址b相对于指令指针(RIP)不断变化(参见行(1)(2)),从而生成更复杂的汇编代码,而不是相对于特定的部分可执行文件,这些变量存储在哪里?

根据所提到的教程,存在这样的部分:

这是因为值的值在示例可执行文件b不同部分中进行了硬编码,并且在启动进程时,操作系统的加载程序将其与所有机器代码一起加载到内存中.

(强调我的.)

Ros*_*dge 8

使用RIP相对寻址访问静态变量有两个主要原因b.第一个是它使代码位置独立,这意味着如果它在共享库中使用或位置独立可执行,则代码可以更容易地重新定位.第二个是它允许将代码加载到64位地址空间中的任何位置,而不需要在指令中编码大的8字节(64位)位移,而64位x86 CPU不支持这些位移.

您提到编译器可以生成相对于它所在部分的开头引用变量的代码.虽然它的真实做法也具有与上面给出的相同的优点,但它不会使组件变得不那么复杂.实际上它会使它变得更复杂.生成的汇编代码首先必须计算变量所在部分的地址,因为它只知道它相对于指令指针的位置.然后它必须将它存储在寄存器中,因此b可以相对于该地址访问(以及该部分中的任何其他变量).

由于32位x86代码不支持RIP相对寻址,因此您的备用解决方案是编译器在生成32位位置无关代码时所执行的操作.它将变量b放在全局偏移表(GOT)中,然后访问相对于GOT基础的变量.这是使用以下代码编译时代码生成的程序集gcc -m32 -O3 -fPIC -S test.c:

natural_generator:
        call    __x86.get_pc_thunk.cx
        addl    $_GLOBAL_OFFSET_TABLE_, %ecx
        movl    b.1392@GOTOFF(%ecx), %eax
        leal    1(%eax), %edx
        addl    $2, %eax
        movl    %edx, b.1392@GOTOFF(%ecx)
        ret
Run Code Online (Sandbox Code Playgroud)

第一个函数调用将以下指令的地址放在ECX中.下一条指令通过添加GOT从指令开始的相对偏移量来计算GOT的地址.变量ECX现在包含GOT的地址,并在访问b其余代码中的变量时用作基础.

将其与生成的64位代码进行比较gcc -m64 -O3 -S test.c:

natural_generator:
        movl    b.1745(%rip), %eax
        leal    1(%rax), %edx
        addl    $2, %eax
        movl    %edx, b.1745(%rip)
        ret
Run Code Online (Sandbox Code Playgroud)

(代码与你问题中的例子不同,因为优化是打开的.一般来说,只考虑优化输出是一个好主意,因为没有优化,编译器经常生成可怕的代码,做很多无用的事情.还要注意-fPIC不需要使用该标志,因为编译器生成64位位置无关代码.)

请注意64位版本中的汇编指令如何减少,使其成为较不复杂的版本.您还可以看到代码使用少一个寄存器(ECX).虽然它在代码中没有太大的区别,但在一个更复杂的例子中,这是一个可以用于其他东西的寄存器.这使得代码变得更加复杂,因为编译器需要更多地处理寄存器.

  • 这是一个原因 - 但它也更有效,因为在x86-64模式下,CPU直接支持RIP相对寻址.使用GOT的32位等价物需要跳过各种环以确定当前的IP(`get_pc_thunk`东西),然后做一些数学计算GOT的位置.使用RIP相对寻址通过直接支持它作为寻址模式来消除这种复杂性(以及32位变体中的前两个指令). (2认同)