我在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?它不是在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>
答案 0 :(得分:3)
如果您启用了优化,可能会获得更清晰的代码,或者您可能没有。但是,这就是它的作用。
%rbp
寄存器被用作帧指针,即指向堆栈原始顶部的指针。它保存在堆栈中,最后存储和恢复。它不是在x86_64中删除,而是在那里添加; 32位的等价物是%ebp
。
保存此值后,程序通过从堆栈指针中减去,从堆栈中分配16个字节。
然后有一个非常低效的系列副本,它将Oh()
的第一个参数设置为printf()
的第二个参数,并将格式字符串的常量地址(相对于指令指针)设置为printf()
的第一个论点。请记住,在此调用约定中,第一个参数在%rdi
(或%edi
用于32位操作数)中传递,第二个参数在%rsi
中传递。这可以简化为两个指令。
调用printf()
后,程序(不必要地)将返回值保存在堆栈上,恢复堆栈和帧指针,然后返回。
在main()
中,有类似的代码来设置堆栈帧,然后程序保存argc
和argv
(不必要),然后它围绕常量参数移动到{{1通过Oh
进入第一个参数。这可以优化为单个指令。然后它会调用%eax
。返回时,它将其返回值设置为0,清理堆栈,然后返回。
您要询问的代码执行以下操作:在堆栈上存储常量32位值0,将32位值Oh()
保存在堆栈中,保存64位指针{{1在堆栈上(argc
的第一个和第二个参数),并设置它将要调用argv
的函数的第一个参数,它先前已经加载了一个常量。对于这个程序来说,这一切都是不必要的,但是如果需要在调用之后使用main()
和%eax
,那么这些寄存器将被破坏时是必要的。没有充分的理由它使用两个步骤来加载常量而不是一个。
答案 1 :(得分: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)
来自here:“寄存器RBP,RBX和R12-R15是被调用者保存寄存器”,所以如果我们想保存其他寄存器,那么我们必须自己做....
100000f64: mov %edi,-0x8(%rbp)
100000f67: mov %rsi,-0x10(%rbp)
我们不确定为什么我们不只是在%edi中加载它需要开始的调用,但我们现在最好将它移到那里。
100000f6b: mov %eax,%edi
调用该函数:
100000f6d: callq 100000f20 <_Oh>
这是返回值(在%eax中传递),xor是比load 0更小的指令,因此是cmmon优化:
100000f72: xor %eax,%eax
清理我们之前添加的堆栈帧(当我们不使用它们时,不确定为什么我们将这些寄存器保存在其中)
100000f74: add $0x10,%rsp
100000f78: pop %rbp
100000f79: retq