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

我正在按照本教程关于assembly。

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

int natural_generator() { int a = 1; static int b = -1; b += 1; /* (1, 2) */ return a + b; } 

编译到这些汇编指令:

 $ 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  pop %rbp retq End of assembler dump. 

(我添加的行号注释(1)(2)(1, 2) 。)

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

根据上面提到的教程,有这样一个部分:

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

(强调我的。)

使用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 

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

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

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

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

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