9

我正在关注关于组装的本教程。

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

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)

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

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

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

(强调我的。)

4

1 回答 1

9

使用 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 的地址。b变量 ECX 现在包含 GOT 的地址,并在其余代码中访问变量时用作基础。

将其与生成的 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

(代码与您问题中的示例不同,因为优化已打开。一般来说,只查看优化的输出是个好主意,因为如果没有优化,编译器通常会生成糟糕的代码,这些代码会做很多无用的事情。另请注意-fPIC不需要使用该标志,因为编译器无论如何都会生成 64 位位置无关代码。)

请注意,64 位版本中的汇编指令少了两条,因此它的复杂程度更低。您还可以看到代码使用了少一个寄存器 (ECX)。虽然它对您的代码没有太大影响,但在一个更复杂的示例中,它是一个可以用于其他用途的寄存器。这使得代码更加复杂,因为编译器需要对寄存器进行更多的处理。

于 2016-10-30T18:20:55.777 回答