【问题标题】:Disassembly sub from rsp and different function returns来自 rsp 的反汇编子程序和不同的函数返回
【发布时间】:2015-03-02 03:28:33
【问题描述】:

我目前正在使用 gcc、gdb 和程序集并试图理解它。我浏览了一些教程并掌握了一些关键点。

所以我决定用一个小的.c文件,看了一下结果,有些东西不是很清楚。

这是文件:

#include <stdio.h>

void func1(){
    int x = 8;
    int y = x + 5;
}

void func2(){
    int x = 12;
}

void func3(){
    int x = 10+20;
}

void func4(){
    int x;
    x = 1;
}

void func5(){
    int x;
    int y;

    x = 2;
    y = 1;
}

void func6(){
    int x;
    int y;

    x=15;
    y=6;
    y += x;
}

int main(int argc, char *argv[]) {
    func1();
    func2();
    func3();
    func4();
    func5();
    func6();
    return 20;
}

这些是反汇编结果:

Dump of assembler code for function main:
0x0000000100000f60 <+0> :   push   %rbp
0x0000000100000f61 <+1> :   mov    %rsp,%rbp
0x0000000100000f64 <+4> :   sub    $0x10,%rsp
0x0000000100000f68 <+8> :   movl   $0x0,-0x4(%rbp)
0x0000000100000f6f <+15>:   mov    %edi,-0x8(%rbp)
0x0000000100000f72 <+18>:   mov    %rsi,-0x10(%rbp)
0x0000000100000f76 <+22>:   callq  0x100000ed0 <func1>
0x0000000100000f7b <+27>:   callq  0x100000ef0 <func2>
0x0000000100000f80 <+32>:   callq  0x100000f00 <func3>
0x0000000100000f85 <+37>:   callq  0x100000f10 <func4>
0x0000000100000f8a <+42>:   callq  0x100000f20 <func5>
0x0000000100000f8f <+47>:   callq  0x100000f40 <func6>
0x0000000100000f94 <+52>:   mov    $0x14,%eax
0x0000000100000f99 <+57>:   add    $0x10,%rsp
0x0000000100000f9d <+61>:   pop    %rbp
0x0000000100000f9e <+62>:   retq

Dump of assembler code for function func1:
0x0000000100000ed0 <+0> :   push   %rbp
0x0000000100000ed1 <+1> :   mov    %rsp,%rbp
0x0000000100000ed4 <+4> :   movl   $0x8,-0x4(%rbp)
0x0000000100000edb <+11>:   mov    -0x4(%rbp),%eax
0x0000000100000ede <+14>:   add    $0x5,%eax
0x0000000100000ee3 <+19>:   mov    %eax,-0x8(%rbp)
0x0000000100000ee6 <+22>:   pop    %rbp
0x0000000100000ee7 <+23>:   retq
0x0000000100000ee8 <+24>:   nopl   0x0(%rax,%rax,1)

Dump of assembler code for function func2:
0x0000000100000ef0 <+0> :   push   %rbp
0x0000000100000ef1 <+1> :   mov    %rsp,%rbp
0x0000000100000ef4 <+4> :   movl   $0xc,-0x4(%rbp)
0x0000000100000efb <+11>:   pop    %rbp
0x0000000100000efc <+12>:   retq
0x0000000100000efd <+13>:   nopl   (%rax)

Dump of assembler code for function func3:
0x0000000100000f00 <+0> :   push   %rbp
0x0000000100000f01 <+1> :   mov    %rsp,%rbp
0x0000000100000f04 <+4> :   movl   $0x1e,-0x4(%rbp)
0x0000000100000f0b <+11>:   pop    %rbp
0x0000000100000f0c <+12>:   retq
0x0000000100000f0d <+13>:   nopl   (%rax)

Dump of assembler code for function func4:
0x0000000100000f10 <+0> :   push   %rbp
0x0000000100000f11 <+1> :   mov    %rsp,%rbp
0x0000000100000f14 <+4> :   movl   $0x1,-0x4(%rbp)
0x0000000100000f1b <+11>:   pop    %rbp
0x0000000100000f1c <+12>:   retq
0x0000000100000f1d <+13>:   nopl   (%rax)

Dump of assembler code for function func5:
0x0000000100000f20 <+0> :   push   %rbp
0x0000000100000f21 <+1> :   mov    %rsp,%rbp
0x0000000100000f24 <+4> :   movl   $0x2,-0x4(%rbp)
0x0000000100000f2b <+11>:   movl   $0x1,-0x8(%rbp)
0x0000000100000f32 <+18>:   pop    %rbp
0x0000000100000f33 <+19>:   retq
0x0000000100000f34 <+20>:   data16 data16 nopw %cs:0x0(%rax,%rax,1)

Dump of assembler code for function func6:
0x0000000100000f40 <+0> :   push   %rbp
0x0000000100000f41 <+1> :   mov    %rsp,%rbp
0x0000000100000f44 <+4> :   movl   $0xf,-0x4(%rbp)
0x0000000100000f4b <+11>:   movl   $0x6,-0x8(%rbp)
0x0000000100000f52 <+18>:   mov    -0x4(%rbp),%eax
0x0000000100000f55 <+21>:   mov    -0x8(%rbp),%ecx
0x0000000100000f58 <+24>:   add    %eax,%ecx
0x0000000100000f5a <+26>:   mov    %ecx,-0x8(%rbp)
0x0000000100000f5d <+29>:   pop    %rbp
0x0000000100000f5e <+30>:   retq
0x0000000100000f5f <+31>:   nop

我编译这个:

gcc  -o example example.c

我不清楚一些事情:

  1. 如果所有函数的结尾都相同(例如在代码中返回 void)为什么
    • func1nopl 0x0(%rax,%rax,1) 结尾
    • func2 & func3 & func4nopl (%rax) 结尾
    • func6nop 结尾
    • func5data16 data16 nopw %cs:0x0(%rax,%rax,1) 结尾。
  2. data16 data16 nopw %cs:0x0(%rax,%rax,1) 到底是什么意思?
  3. 主要有
    • 低于 $0x10,%rsp
    • 添加 $0x10,%rsp
    • 这些是为方法中的局部变量分配内存吗?如果是这样,为什么它们总是四舍五入到 0x10、0x20、0x30……这不是有点浪费吗?

【问题讨论】:

    标签: c gcc assembly gdb disassembly


    【解决方案1】:

    所有这些nopl 0x0(%rax,%rax,1) 等指令都是nop 指令的变体。它们用于确保函数的长度是 16 字节的倍数。你可能会问他们为什么不只使用多个0x90 (nop) 指令。答案是,如果正在执行这些 nop,执行一个长的多字节 nop(如 data16 data16 nopw %cs:0x0(%rax,%rax,1)nopl (%rax))会比执行多个短的 nop 稍微快一些。 Nop 出现在函数内部时可能会被执行;当编译器想要对齐跳转目标以提高性能时,会生成这样的代码。 nop 是由不知道哪些 nop 可以执行,哪些 nop 不会执行的汇编程序生成的,因为这通常是不可确定的。

    关于堆栈的部分:您正在编译没有优化,您不应该询问没有优化生成的奇怪代码。编译器在没有优化的情况下被指示不聪明,那你为什么期望它节省空间呢?

    【讨论】:

    • 所以data16是16位的?而 func5 错过了 32bit + nopw 来完成 16Byte? nopw 是多少?
    • @MP_ 编译器只插入指令来对齐函数之间的代码。这就是为什么最后一个函数没有被填充的原因。链接器实际上确保每个目标文件的文本部分最终正确对齐。
    • 不同长度的NOP列表可以在here找到
    • 顺便说一句:这可以通过 -falign-functions=n GCC 开关来控制。
    • @MP_ data16 是使指令在 16 位寄存器而不是 32 位寄存器上操作的前缀。通常,当您看到像 movw %ax,%bx 这样的指令时,此前缀是隐含的,但如果它多次出现,反汇编程序会拼出它,因此反汇编是明确的。
    【解决方案2】:
    1. 函数以retq 语句结尾。反汇编中显示的操作码对于实际执行来说只是垃圾,但可能在现代预测 CPU 中被预先执行(并丢弃)。你可以忽略它们。有针对具有“分支延迟”的其他 CPU 的说明,但 x86 没有此功能。 retq 和下一个 16 字节边界之间的间隙可以让函数从偶数地址开始。这样可以加快执行速度。

    2. data16 可能意味着有一个 16 位数据与反汇编程序已知的任何操作码都不匹配。忽略它,不会影响执行。

    3. x86 架构允许访问任何地址而不考虑对齐。但是访问未对齐的变量可能需要一个以上的总线周期来访问内存。堆栈点rsp 的对齐保证了对 uint64_t 的访问只会导致一个总线周期。

    【讨论】:

    • ret 后面的操作码不是垃圾;看我的回答。它们实际上是精心制作的 nop。 data16 是用于使 nop 稍长的前缀。
    • 最后,如果我没记错的话,amd64 Linux 的 SysV ABI 要求堆栈在函数入口处与 16 个字节对齐。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-05
    • 1970-01-01
    • 2014-09-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多