【问题标题】:Compilers: Understanding assembly code generated from small programs编译器:了解从小程序生成的汇编代码
【发布时间】:2017-08-17 01:56:26
【问题描述】:

我正在自学编译器的工作原理。我通过阅读从小型 64 位 Linux 程序生成的 GCC 代码的反汇编来学习。

我写了这个C程序:

#include <stdio.h>

int main()
{
    for(int i=0;i<10;i++){
        int k=0;
    }
}

使用 objdump 后我得到:

00000000004004d6 <main>:
  4004d6:       55                      push   rbp
  4004d7:       48 89 e5                mov    rbp,rsp
  4004da:       c7 45 f8 00 00 00 00    mov    DWORD PTR [rbp-0x8],0x0
  4004e1:       eb 0b                   jmp    4004ee <main+0x18>
  4004e3:       c7 45 fc 00 00 00 00    mov    DWORD PTR [rbp-0x4],0x0
  4004ea:       83 45 f8 01             add    DWORD PTR [rbp-0x8],0x1
  4004ee:       83 7d f8 09             cmp    DWORD PTR [rbp-0x8],0x9
  4004f2:       7e ef                   jle    4004e3 <main+0xd>
  4004f4:       b8 00 00 00 00          mov    eax,0x0
  4004f9:       5d                      pop    rbp
  4004fa:       c3                      ret    
  4004fb:       0f 1f 44 00 00          nop    DWORD PTR [rax+rax*1+0x0]

现在我有些怀疑了。

  1. 最后的 NOP 是什么,为什么会出现? (对齐?)

  2. 我正在使用gcc -Wall &lt;program.c&gt; 进行编译。为什么我没有收到警告 control reaches end of non-void function

  3. 为什么编译器不使用sub rsp,0x10 在堆栈上分配空间?为什么不使用rbp 寄存器来引用本地堆栈数据?

    PS:如果我在for循环中调用了一个函数(比如printf),为什么编译器会突然生成sub rsp,0x10?为什么它仍然使用rsp 寄存器引用本地数据。我希望生成的代码使用rbp! 引用本地堆栈数据!

【问题讨论】:

  • 一个小提示:如果你想玩转编译器,请查看:godbolt.org
  • 没有一个答案能真正说明为什么会出现 #3。您将在 Linux 64 位代码中找到它。编译器正在利用Red Zone。您可以在64-bit System V Linux ABI 找到这个红色区域的信息。在该文档的副本中查看第 3.2.2 节:堆栈帧。由于您的函数是叶函数(不调用其他函数),它可以利用低于当前 128 个字节的事实RSP 不会被信号处理等破坏。
  • 因为 RSP 的当前值以下 128 字节(在叶函数中)是安全的,如果函数堆栈,则无需调整 RSP基于数据的数据可以容纳在这 128 个字节中。这就是您在此处看不到 RSP 调整的原因。一项观察。 Red Zone 在 32 位 Linux 中不存在,因此编译为 32 位代码应该会看到预期的行为。
  • 至于为什么当你添加 test 函数时编译器减去 16 个字节,尽管需要更少的字节与 64 位 Linux ABI 要求有关,在任何函数调用时(就像调用test)堆栈需要是16(并且可能是32字节对齐)。由于堆栈在调用点对齐,因此 CALL 本身会推送 8 字节的返回地址。这会使堆栈错位 8。push rbp 减去另一个 8,使其再次对齐 16 字节。现在您需要局部变量数据。编译器通常会分配足够的字节来保持 16 字节对齐
  • 减去 16 字节,当它到达对 test 的调用时,堆栈仍然保持 16 字节对齐,一切都很好..

标签: c linux gcc x86-64 disassembly


【解决方案1】:

关于第二个问题,由于 C99 标准允许在 main 函数中没有显式的 return 0,编译器将隐式添加它。请注意,这仅适用于main 函数,不适用于其他函数。

至于第三个问题,rbp寄存器充当frame pointer

最后是PS。被调用函数很可能使用16 字节(0x10)作为传递给函数的参数。减法是从堆栈中“删除”这些变量。它可能是您作为参数传递的两个指针吗?

如果您正在认真学习编译器的一般工作原理,并且可能想要创建自己的编译器(这很有趣!:)),那么我建议您购买一些有关其理论和实践的书籍。 The dragon book 是任何程序员书架的绝佳补充。

【讨论】:

【解决方案2】:
  1. 是的,nop 用于对齐。编译器对所需的不同填充长度使用不同的指令,因为他们知道现代 CPU 会提前预取和解码多条指令。

  2. 正如其他人所说,如果没有明确的 return 语句,C99 标准默认从 main() 返回 0(请参阅 C99 TC3 中的 5.1.2.2.3),因此不会引发警告。

  3. 64-bit System V Linux ABI 在当前堆栈指针下方保留一个 128 字节的“红色区域”,叶函数(不调用任何其他函数的函数 - 您的 main() 就是这样的函数)可以用于本地变量和其他临时值,而无需子 rsp / 添加 rsp。所以 rbp == rsp。

对于 PS:当您在 for() 循环中(或 main() 中的任何位置)调用函数时,main() 不再是叶函数,因此编译器不能再使用红色区域。这就是它使用 sub rsp, 0x10 在堆栈上分配空间的原因。但是它知道rsp和rbp之间的关系,所以在访问数据时可以使用其中任何一个。

【讨论】:

    【解决方案3】:

    ret 之后的任何内容都不能被依赖为代码。解码为nop 表示“无操作”

    第二点是编译器检测到您离开 main 函数而不返回值,它会插入一个 return 0(仅为 main 定义)。

    rbp 寄存器,bp 意思是“基指针”,指向当前函数的栈帧。函数调用通常会导致函数条目保存rbp 并将rsp 的当前值用于rbp。获取/存储函数参数和局部变量是相对于rbp 完成的。


    我认为您的第三个问题需要更多关注,“为什么编译器不使用 sub rsp,0x10 在堆栈上分配空间?为什么不使用 rbp 寄存器来引用本地堆栈数据?

    实际上,编译器确实在堆栈上分配空间。但它不会改变堆栈指针。它可以做到这一点,因为该函数不调用其他函数。它只使用当前sp(堆栈向下增长)下方的空间,并使用rbp 访问i[rbp-0x8])和k[rbp-0x4])。


    我必须添加以下注意事项:不调整 sp 以使用局部变量似乎 不是中断安全,因此编译器依赖于硬件在中断发生时自动切换到系统堆栈。否则,出现的第一个中断会将指令指针压入堆栈并覆盖局部变量。

    解决的中断问题 Compiler using local variables without adjusting RSP

    【讨论】:

    • 在ret调用之后,编译器只是抛出随机操作码?
    • @Ofey :这里的任何答案都没有捕捉到 NOP 存在的原因。在大多数情况下,当它在一个函数之后(在这种情况下是在ret 之后),它被放置在那里,以便下一个函数从 16 字节边界开始。获取 NOP 开头的地址(objdump 中的 0x4004fb)并添加 NOP 占用的 5 个字节。你得到 0x400500。 0x400500 可以被 16 整除。这样做是出于性能原因(与缓存行有关)。很可能在您的 objdump 中,您有一个出现在main 之后的函数。您会在函数中看到 NOP,以将这样的循环对齐为好吧。
    • @MichaelPetch 谢谢!通常当我看到nop 时,我总是会想到对齐,在这种情况下,我无法理解对齐的位置。再次感谢您!
    • 我还使用nops 来修补可执行文件。
    • @PaulOgilvie Me 也是,在一些破解挑战中。
    猜你喜欢
    • 2017-08-18
    • 2014-05-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-04-01
    • 1970-01-01
    • 2018-10-22
    • 2014-10-29
    相关资源
    最近更新 更多