尝试理解汇编代码中的调用过程
Try to understand calling process in assembly code
我用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 [=11=]x10,%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 [=11=]x0,%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 [=11=]x10,%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 [=11=]x10,%rsp
100000f58: b8 8c 1c 61 67 mov [=11=]x67611c8c,%eax
100000f5d: c7 45 fc 00 00 00 00 movl [=11=]x0,-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 [=11=]x10,%rsp
100000f78: 5d pop %rbp
100000f79: c3 retq
嗯,我不太明白参数传递过程,因为只有一个参数传递给Oh函数,我可以理解这个
100000f58: b8 8c 1c 61 67 mov [=12=]x67611c8c,%eax
下面的代码有什么作用?为什么是限制性商业惯例?它不是在 X86-64 汇编中被遗弃了吗?如果是 x86 风格的程序集,如何使用 clang 生成 x86-64 风格的程序集?如果是x86,没关系,谁能帮我逐行解释下面的代码?
100000f5d: c7 45 fc 00 00 00 00 movl [=13=]x0,-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>
如果打开优化,您可能会获得更清晰的代码,也可能不会。但是,这就是它的作用。
%rbp
寄存器被用作帧指针,即指向原始栈顶的指针。它保存在堆栈中,存储并在最后恢复。它远没有在 x86_64 中被删除,而是被添加到那里; 32 位等效项是 %ebp
.
保存此值后,程序通过从堆栈指针中减去堆栈分配十六个字节。
然后是一系列非常低效的拷贝,将Oh()
的第一个参数设置为printf()
的第二个参数和格式字符串的常量地址(相对于指令指针)作为 printf()
的第一个参数。请记住,在此调用约定中,第一个参数在 %rdi
中传递(或 %edi
用于 32 位操作数),第二个参数在 %rsi
中传递。这可以简化为两条指令。
调用printf()
后,程序(不必要地)将return值保存在栈上,恢复栈和帧指针,returns.
在main()
中,有类似的代码来设置堆栈帧,然后程序保存argc
和argv
(不必要),然后将常量参数移动到Oh
通过 %eax
进入它的第一个参数。这本可以优化为一条指令。然后调用 Oh()
。在 return 上,它将其 return 值设置为 0,清理堆栈,然后 returns.
您询问的代码执行以下操作:将常量 32 位值 0 存储在堆栈上,将 32 位值 argc
保存在堆栈上,将 64 位指针 argv
放在堆栈上(main()
的第一个和第二个参数),并将它要调用的函数的第一个参数设置为 %eax
,它之前已经加载了一个常量。这对这个程序来说都是不必要的,但如果在调用后需要使用 argc
和 argv
,那么这些寄存器将被破坏,这将是必要的。没有充分的理由使用两步而不是一步来加载常量。
正如 Jester 提到的,您仍然有帧指针(以帮助调试),因此逐步执行 main:
0000000100000f50 <_main>:
首先我们进入一个新的栈帧,我们必须保存基址指针并将栈移动到新的基址。此外,在 x86_64 中,堆栈帧必须与 16 字节边界对齐(因此将堆栈指针移动 0x10)。
100000f50: push %rbp
100000f51: mov %rsp,%rbp
100000f54: sub [=11=]x10,%rsp
正如你所说,x86_64通过寄存器传递参数,所以将参数加载到寄存器中:
100000f58: mov [=12=]x67611c8c,%eax
???需要帮助
100000f5d: movl [=13=]x0,-0x4(%rbp)
来自here:"Registers RBP, RBX, and R12-R15 are callee-save registers",所以如果我们想拯救其他抵抗者,那么我们必须自己动手....
100000f64: mov %edi,-0x8(%rbp)
100000f67: mov %rsi,-0x10(%rbp)
不太清楚为什么我们不把它加载到 %edi 中,它需要在调用开始的地方,但我们现在最好把它移到那里。
100000f6b: mov %eax,%edi
调用函数:
100000f6d: callq 100000f20 <_Oh>
这是 return 值(在 %eax 中传递),xor 是比加载 0 更小的指令,因此是 cmmon 优化:
100000f72: xor %eax,%eax
清理我们之前添加的堆栈帧(不太确定为什么我们在不使用它们时将这些寄存器保存在上面)
100000f74: add [=18=]x10,%rsp
100000f78: pop %rbp
100000f79: retq
我用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 [=11=]x10,%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 [=11=]x0,%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 [=11=]x10,%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 [=11=]x10,%rsp
100000f58: b8 8c 1c 61 67 mov [=11=]x67611c8c,%eax
100000f5d: c7 45 fc 00 00 00 00 movl [=11=]x0,-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 [=11=]x10,%rsp
100000f78: 5d pop %rbp
100000f79: c3 retq
嗯,我不太明白参数传递过程,因为只有一个参数传递给Oh函数,我可以理解这个
100000f58: b8 8c 1c 61 67 mov [=12=]x67611c8c,%eax
下面的代码有什么作用?为什么是限制性商业惯例?它不是在 X86-64 汇编中被遗弃了吗?如果是 x86 风格的程序集,如何使用 clang 生成 x86-64 风格的程序集?如果是x86,没关系,谁能帮我逐行解释下面的代码?
100000f5d: c7 45 fc 00 00 00 00 movl [=13=]x0,-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>
如果打开优化,您可能会获得更清晰的代码,也可能不会。但是,这就是它的作用。
%rbp
寄存器被用作帧指针,即指向原始栈顶的指针。它保存在堆栈中,存储并在最后恢复。它远没有在 x86_64 中被删除,而是被添加到那里; 32 位等效项是 %ebp
.
保存此值后,程序通过从堆栈指针中减去堆栈分配十六个字节。
然后是一系列非常低效的拷贝,将Oh()
的第一个参数设置为printf()
的第二个参数和格式字符串的常量地址(相对于指令指针)作为 printf()
的第一个参数。请记住,在此调用约定中,第一个参数在 %rdi
中传递(或 %edi
用于 32 位操作数),第二个参数在 %rsi
中传递。这可以简化为两条指令。
调用printf()
后,程序(不必要地)将return值保存在栈上,恢复栈和帧指针,returns.
在main()
中,有类似的代码来设置堆栈帧,然后程序保存argc
和argv
(不必要),然后将常量参数移动到Oh
通过 %eax
进入它的第一个参数。这本可以优化为一条指令。然后调用 Oh()
。在 return 上,它将其 return 值设置为 0,清理堆栈,然后 returns.
您询问的代码执行以下操作:将常量 32 位值 0 存储在堆栈上,将 32 位值 argc
保存在堆栈上,将 64 位指针 argv
放在堆栈上(main()
的第一个和第二个参数),并将它要调用的函数的第一个参数设置为 %eax
,它之前已经加载了一个常量。这对这个程序来说都是不必要的,但如果在调用后需要使用 argc
和 argv
,那么这些寄存器将被破坏,这将是必要的。没有充分的理由使用两步而不是一步来加载常量。
正如 Jester 提到的,您仍然有帧指针(以帮助调试),因此逐步执行 main:
0000000100000f50 <_main>:
首先我们进入一个新的栈帧,我们必须保存基址指针并将栈移动到新的基址。此外,在 x86_64 中,堆栈帧必须与 16 字节边界对齐(因此将堆栈指针移动 0x10)。
100000f50: push %rbp
100000f51: mov %rsp,%rbp
100000f54: sub [=11=]x10,%rsp
正如你所说,x86_64通过寄存器传递参数,所以将参数加载到寄存器中:
100000f58: mov [=12=]x67611c8c,%eax
???需要帮助
100000f5d: movl [=13=]x0,-0x4(%rbp)
来自here:"Registers RBP, RBX, and R12-R15 are callee-save registers",所以如果我们想拯救其他抵抗者,那么我们必须自己动手....
100000f64: mov %edi,-0x8(%rbp)
100000f67: mov %rsi,-0x10(%rbp)
不太清楚为什么我们不把它加载到 %edi 中,它需要在调用开始的地方,但我们现在最好把它移到那里。
100000f6b: mov %eax,%edi
调用函数:
100000f6d: callq 100000f20 <_Oh>
这是 return 值(在 %eax 中传递),xor 是比加载 0 更小的指令,因此是 cmmon 优化:
100000f72: xor %eax,%eax
清理我们之前添加的堆栈帧(不太确定为什么我们在不使用它们时将这些寄存器保存在上面)
100000f74: add [=18=]x10,%rsp
100000f78: pop %rbp
100000f79: retq