我关注装配this tutorial。
根据教程(我也在本地尝试过,得到了类似的结果),以下源代码:
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 <natural_generator.b> pop %rbp retq End of assembler dump.
(我添加的行号评论(1)
,(2)
和(1, 2)
。)
问题:为什么在编译代码中,相对于指令指针(RIP)的静态变量b
的地址不断变化(请参阅第(1)
行和(2)
行),从而生成更复杂的汇编代码,而不是相对于存储此类变量的可执行文件的特定部分?
根据上述教程,是这样的部分:
这是因为
b
的值在不同部分中被硬编码 示例可执行文件,它与所有的一起加载到内存中 机器代码由操作系统的加载程序进行处理 启动。
(强调我的。)
答案 0 :(得分:8)
使用RIP相对寻址访问静态变量b
有两个主要原因。第一个是它使代码位置独立,这意味着如果它在共享库中使用或position independent executable代码可以更容易地重新定位。第二个是它允许将代码加载到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)。虽然它在代码中没有太大的区别,但在一个更复杂的例子中,它是一个可以被用于其他东西的寄存器。这使得代码变得更加复杂,因为编译器需要更多地处理寄存器。