Att*_*lio 6 c assembly gcc cpu-registers
我正在按照本教程关于装配.
根据教程(我也在本地尝试,并得到类似的结果),以下源代码:
Run Code Online (Sandbox Code Playgroud)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.
(行号的意见(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
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).虽然它在代码中没有太大的区别,但在一个更复杂的例子中,这是一个可以用于其他东西的寄存器.这使得代码变得更加复杂,因为编译器需要更多地处理寄存器.