栈
一种受限制的线性表,栈底为高地址,栈顶为低地址。
重要寄存器
ebp:即MIPS中的fp,用来存储当前函数的基地址
esp:即MIPS中的sp,用来存储函数调用栈的栈顶地址,在出栈和压栈时发生变化
C代码
#include <stdio.h>
int Add(int x, int y) {
int z = 0;
z = x + y;
return z;
}
int main() {
int a = 10;
int b = 20;
int ret = Add(a, b);
printf("ret = %d\n", ret);
return 0;
}
环境
CPU配置:Intel(R) Core(TM) i7-6700HQ @2.60GHz 2.59GHz
内存配置:8.00GB
操作系统名称及版本号:Windows 10 家庭中文版 版本1803
内核版本号:OS内部版本 17134.81
DevC++版本号:5.11
main函数的汇编代码
0x0000000000401559 <+0>: push rbp
0x000000000040155a <+1>: mov rbp,rsp
0x000000000040155d <+4>: sub rsp,0x30
0x0000000000401561 <+8>: call 0x402120 <__main>
0x0000000000401566 <+13>: mov DWORD PTR [rbp-0x4],0xa
0x000000000040156d <+20>: mov DWORD PTR [rbp-0x8],0x14
0x0000000000401574 <+27>: mov edx,DWORD PTR [rbp-0x8]
0x0000000000401577 <+30>: mov eax,DWORD PTR [rbp-0x4]
0x000000000040157a <+33>: mov ecx,eax
0x000000000040157c <+35>: call 0x401530 <Add(int, int)>
0x0000000000401581 <+40>: mov DWORD PTR [rbp-0xc],eax
0x0000000000401584 <+43>: mov eax,DWORD PTR [rbp-0xc]
0x0000000000401587 <+46>: mov edx,eax
0x0000000000401589 <+48>: lea rcx,[rip+0x2a70] # 0x404000
0x0000000000401590 <+55>: call 0x402b38 <printf>
0x0000000000401595 <+60>: mov eax,0x0
0x000000000040159a <+65>: add rsp,0x30
0x000000000040159e <+69>: pop rbp
0x000000000040159f <+70>: ret
Add函数的汇编代码
0x0000000000401530 <+0>: push rbp
0x0000000000401531 <+1>: mov rbp,rsp
0x0000000000401534 <+4>: sub rsp,0x10
0x0000000000401538 <+8>: mov DWORD PTR [rbp+0x10],ecx
0x000000000040153b <+11>: mov DWORD PTR [rbp+0x18],edx
0x000000000040153e <+14>: mov DWORD PTR [rbp-0x4],0x0
0x0000000000401545 <+21>: mov edx,DWORD PTR [rbp+0x10]
0x0000000000401548 <+24>: mov eax,DWORD PTR [rbp+0x18]
0x000000000040154b <+27>: add eax,edx
0x000000000040154d <+29>: mov DWORD PTR [rbp-0x4],eax
0x0000000000401550 <+32>: mov eax,DWORD PTR [rbp-0x4]
0x0000000000401553 <+35>: add rsp,0x10
0x0000000000401557 <+39>: pop rbp
0x0000000000401558 <+40>: ret
通过汇编代码理解过程调用
首先,我们来看C代码,主函数中先定义一个a,b然后调用函数Add,实现相加返回。
先看初始化部分
0x0000000000401559 <+0>: push rbp
0x000000000040155a <+1>: mov rbp,rsp
0x000000000040155d <+4>: sub rsp,0x30
0x0000000000401561 <+8>: call 0x402120 <__main>
main函数刚开始运行前,进行初始化,先将rbp压栈,再将ebp的值赋给rsp,rsp通过sub指令减去0x30,即开辟空间,接着跳转至main函数,并设置返回地址。
0x0000000000401566 <+13>: mov DWORD PTR [rbp-0x4],0xa
0x000000000040156d <+20>: mov DWORD PTR [rbp-0x8],0x14
0x0000000000401574 <+27>: mov edx,DWORD PTR [rbp-0x8]
0x0000000000401577 <+30>: mov eax,DWORD PTR [rbp-0x4]
0x000000000040157a <+33>: mov ecx,eax
0x000000000040157c <+35>: call 0x401530 <Add(int, int)>
接着是a,b的压栈过程,前2句指令表示在之前开辟的空间内压栈2个值,分别为0xa,0x14,表示十进制的10和20;
这2句指令对应的C代码为int a = 10; int b = 20;
接着是将栈中的值赋给edx和eax寄存器,再把eax寄存器的值赋给ecx,这里我的理解是为传参过程进行准备
一顿操作之后,ecx寄存器的值为10,edx寄存器的值为20。紧接着是call指令,跳转至Add函数,同时留下返回地址。
0x0000000000401530 <+0>: push rbp
0x0000000000401531 <+1>: mov rbp,rsp
0x0000000000401534 <+4>: sub rsp,0x10
跳转到Add函数前,和main函数一样,进行初始化操作,对rbp进行压栈,此时的rbp为main函数的栈顶,Add函数的栈基,类似的操作,进行赋值后减去0x10,开辟空间。
0x0000000000401538 <+8>: mov DWORD PTR [rbp+0x10],ecx
0x000000000040153b <+11>: mov DWORD PTR [rbp+0x18],edx
0x000000000040153e <+14>: mov DWORD PTR [rbp-0x4],0x0
这部分我认为是所有代码中最难理解得到一段,即压栈位置是在main函数的栈帧之中,查阅资料得到,形参的实例化是在上一函数的栈帧中完成的(这里是main函数),前面分析的ecx寄存器的值是10,edx寄存器的值是20,所以这2条指令的含义就是对a,b进行压栈,实例化(存储的地址有别于之前的a,b的地址,保证形参不影响实参),第三条指令就是局部变量的压栈,对应C代码中的int z = 0;
可以看到,实例化形参a,b的地址是b在高地址,a在高地址,栈是先进后出的,故我们就不难理解为什么C/C++中参数的传递顺序的从右到左了。
0x0000000000401545 <+21>: mov edx,DWORD PTR [rbp+0x10]
0x0000000000401548 <+24>: mov eax,DWORD PTR [rbp+0x18]
0x000000000040154b <+27>: add eax,edx
0x000000000040154d <+29>: mov DWORD PTR [rbp-0x4],eax
0x0000000000401550 <+32>: mov eax,DWORD PTR [rbp-0x4]
0x0000000000401553 <+35>: add rsp,0x10
0x0000000000401557 <+39>: pop rbp
0x0000000000401558 <+40>: ret
接着的代码就好理解了,将实例化后的值赋给2个寄存器,再进行add指令进行相加,结果存储在eax寄存器中,再将eax寄存器的赋给z;最后rsp加0x10,清除空间,返回,结束过程调用!
(tips:之所以有很多寄存器与栈之间的相互赋值,是因为mov指令不能直接进行2个已经有值得寄存器直接的赋值操作,也不能读取栈的数据赋到另一个地址中)