尝试理解汇编代码中的调用过程

3

我用C语言编写了一个非常简单的程序,并尝试理解函数调用过程。

#include "stdio.h"

void Oh(unsigned x) {
    printf("%u\n", x);
}

int main(int argc, char const *argv[])
{
    Oh(0x67611c8c);
    return 0;
}

它的汇编代码似乎是

0000000100000f20 <_Oh>:
   100000f20:   55                      push   %rbp
   100000f21:   48 89 e5                mov    %rsp,%rbp
   100000f24:   48 83 ec 10             sub    $0x10,%rsp
   100000f28:   48 8d 05 6b 00 00 00    lea    0x6b(%rip),%rax        # 100000f9a <_printf$stub+0x20>
   100000f2f:   89 7d fc                mov    %edi,-0x4(%rbp)
   100000f32:   8b 75 fc                mov    -0x4(%rbp),%esi
   100000f35:   48 89 c7                mov    %rax,%rdi
   100000f38:   b0 00                   mov    $0x0,%al
   100000f3a:   e8 3b 00 00 00          callq  100000f7a <_printf$stub>
   100000f3f:   89 45 f8                mov    %eax,-0x8(%rbp)
   100000f42:   48 83 c4 10             add    $0x10,%rsp
   100000f46:   5d                      pop    %rbp
   100000f47:   c3                      retq
   100000f48:   0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
   100000f4f:   00

0000000100000f50 <_main>:
   100000f50:   55                      push   %rbp
   100000f51:   48 89 e5                mov    %rsp,%rbp
   100000f54:   48 83 ec 10             sub    $0x10,%rsp
   100000f58:   b8 8c 1c 61 67          mov    $0x67611c8c,%eax
   100000f5d:   c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)
   100000f64:   89 7d f8                mov    %edi,-0x8(%rbp)
   100000f67:   48 89 75 f0             mov    %rsi,-0x10(%rbp)
   100000f6b:   89 c7                   mov    %eax,%edi
   100000f6d:   e8 ae ff ff ff          callq  100000f20 <_Oh>
   100000f72:   31 c0                   xor    %eax,%eax
   100000f74:   48 83 c4 10             add    $0x10,%rsp
   100000f78:   5d                      pop    %rbp
   100000f79:   c3                      retq

嗯,我不是很理解参数传递的过程,因为只有一个参数传递给Oh函数,我可以理解这一点。

100000f58:  b8 8c 1c 61 67          mov    $0x67611c8c,%eax

那么下面的代码是做什么的?为什么要使用 rbp?rbp 在 X86-64 汇编中不是已经被弃用了吗?如果它是 x86 样式的汇编,那么我如何使用 clang 生成 x86-64 样式的汇编?如果它是 x86,那也没关系,请有人逐行解释下面的代码吗?
100000f5d:  c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)
100000f64:  89 7d f8                mov    %edi,-0x8(%rbp)
100000f67:  48 89 75 f0             mov    %rsi,-0x10(%rbp)
100000f6b:  89 c7                   mov    %eax,%edi
100000f6d:  e8 ae ff ff ff          callq  100000f20 <_Oh>

你忘记开启优化(并且可能关闭帧指针,但这通常会自动完成),所以你看到了垃圾代码。 - Jester
你说的“abandoned”是什么意思? - user253751
2个回答

3
如果您打开了优化,代码可能会更干净;但也有可能不会。这是它的作用:
%rbp寄存器被用作帧指针,即指向堆栈原始顶部的指针。它被保存在堆栈上,存储并在最后恢复。在x86_64中,它被添加进去,而32位等效物是%ebp。
保存此值后,程序通过从堆栈指针中减去来分配16个字节的堆栈空间。
然后进行了一系列非常低效的复制,将printf()的第二个参数设置为Oh()的第一个参数,将格式字符串的常量地址(相对于指令指针)设置为printf()的第一个参数。请记住,在这种调用约定中,第一个参数传递到%rdi(或32位操作数的%edi),第二个参数传递到%rsi。这可以简化为两个指令。
调用printf()后,程序(不必要地)将返回值保存在堆栈上,恢复堆栈和帧指针,并返回。
main()中,有类似的代码来设置堆栈帧,然后程序将argcargv保存(不必要地),然后通过%eax将常量参数移动到Oh的第一个参数。这可以优化为单个指令。然后调用Oh()。返回时,将返回值设置为0,清理堆栈并返回。
您正在询问的代码执行以下操作:在堆栈上存储常量32位值0,在堆栈上保存32位值argc,在堆栈上保存64位指针argvmain()的第一个和第二个参数),并将即将调用的函数的第一个参数设置为%eax,它之前已经加载了一个常量。这对于此程序来说都是不必要的,但如果需要在调用后使用argcargv,那么这将是必需的,因为这些寄存器将被破坏。没有充分的理由使用两个步骤来加载常量而不是一个。

嗯,这可能比我的答案更好(我在你发布这个问题时正在写我的答案)……它将16个字节分配给堆栈(0x10),其中一个原因是因为在x86_64中堆栈必须对齐到16个字节(因此最小分配量为16个字节)。 - dave
糟糕,那是十六进制,不是十进制。 - Davislor
做得好。我想要补充一点,你可能会看到 %rbp 寄存器被用作框架指针,也就是指向原始堆栈顶部的指针 -- 被称为汇编 函数序言 (Function Prolog)。(我知道你知道,但其他人可能会遇到这些术语并没有意识到它的含义)。 - David C. Rankin
好的。您会注意到,所有存储在堆栈上而不是寄存器中的临时变量的引用都是相对于“%rbp”的,因为它在函数调用期间不会改变,而堆栈指针可能会改变,例如如果它需要将调用所需的参数推入堆栈中。 - Davislor

2
正如Jester所提到的,您仍然保留了框架指针(以帮助调试),因此在执行main函数时,可以逐步进行:
0000000100000f50 <_main>:

首先,我们进入一个新的堆栈帧,需要保存基指针并将堆栈移动到新的基址。另外,在x86_64中,堆栈帧必须对齐到16字节边界(因此将堆栈指针移动0x10)。

       100000f50:   push   %rbp        
       100000f51:   mov    %rsp,%rbp
       100000f54:   sub    $0x10,%rsp

正如你所提到的,x86_64通过寄存器传递参数,因此需要将参数加载到寄存器中:

       100000f58:   mov    $0x67611c8c,%eax

需要帮助

       100000f5d:   movl   $0x0,-0x4(%rbp)

来自这里的信息:“寄存器RBP、RBX以及R12-R15是被调用者保存的寄存器”,因此如果我们想要保存其他寄存器,就必须自己手动保存...

       100000f64:   mov    %edi,-0x8(%rbp)
       100000f67:   mov    %rsi,-0x10(%rbp)

我不是很确定为什么我们没有在调用函数之前将它加载到%edi中,但现在我们最好把它移到那里。

       100000f6b:   mov    %eax,%edi

调用函数:

       100000f6d:   callq  100000f20 <_Oh>

这是返回值(在%eax中传递的),异或指令比加载0指令更小,因此是一种常见的优化:

       100000f72:   xor    %eax,%eax

清理之前添加的堆栈帧(不确定为什么要保存那些寄存器,但实际上并没有使用它们)。
       100000f74:   add    $0x10,%rsp
       100000f78:   pop    %rbp
       100000f79:   retq

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接