Pwn入门系列9——函数调用流程与调用约定

函数调用流程

1.png
(1)当运行到call func_b时main函数的栈帧
(2)RBP指向栈顶,rsp指向栈顶
(3)这段栈帧存放了一些main的局部变量
(4)main函数要调用func_b,main只需要call func_b
(5)也就是push rip;mov rip func_b;
2.png
(6)那么此时跳转到func_b继续执行,func_b直接执行主逻辑吗?
(7)显然不是的,被调用函数还要维护栈帧。
(8)具体来说,需要以下几步
①push rbp:将调用函数的栈底指针保存
②mov rbp rsp:将栈底指针指向现在的栈顶
③sub rsp xxx;开辟被调用函数的栈帧,此时上一步的rbp就指向栈帧的底部。
(9)func_b执行完维护栈帧操作后的栈布局
(10)所谓栈帧的维护就是维护rbp和rsp两个指针
(11)RSP永远指向栈顶
(12)RBP用来定位局部变量
3.png
(13)现在,func_b要调用func_a,其调用流程与main函数调用func_b基本一致
(14)不同之处在于返回地址、rbp和rsp指向的地址,以及开辟的栈空间不同
(15)func_b调用完func_a后的栈布局
4.png
(16)至此,示例的函数调用已经完毕
(17)现在,func_a执行完毕,要返回了
(18)该如何维护栈帧呢?
(19)Leave指令:
①作用是维护栈帧,通常出现在函数的结尾,与ret连用
②其实际作用为:mov rsp rbp;pop rbp;
③即:将栈顶指针指向栈帧底部,然后在栈中弹出新的栈底指针。
(20)在一个函数执行结束返回时,会执行leave;ret
(21)实际效果就是:mov rsp rbp;pop rbp;pop eip
(22)观察程序执行至func_a时的栈帧。
5.png
(23)func_a执行完毕返回后,栈布局如图:
6.png
(24)可以与之前func_b未调用func_a前的栈帧对比
(25)一模一样,说明已经恢复了栈帧
(26)唯一不同处就在于此程序的rip已经指向了c=1
(27)func_b执行完毕返回后,栈布局如图:
7.png
(28)在这之后,main函数继续执行,直到结束。

函数调用流程总结

①调用函数:只需要将rip压栈,即push rip,然后将rip赋值为被调用函数的起始地址,这已操作被隐性的内置在call指令中。
②被调用函数:push rbp;mov rbp rsp;sub rsp 0xxx。即保存调用函数的rbp指针,将自己的rbp指针指向栈顶,然后开辟栈空间给自己用,此时就变成了被调用函数的栈底。
③函数返回:leave;ret;翻译过来就是:mov rsp rbp;pop rbp;pop rip;
即恢复栈帧,返回调用函数的返回地址。

调用约定

(1)返回值:一般来说,一个函数的返回值会存储到RAX寄存器
(2)X86-64函数的调用约定为:
①从左至右参数一次传递给rdi,rsi,rdx,rcx,r8,r9
②如果一个函数的参数多于6个,则其余的参数从右至左压入栈中传递。
(3)syscall指令:
①用于调用系统函数,调用时需要知名操作系统调用号
②系统调用号在rax寄存器中,然后布置好参数,执行syscall即可。
8.png
9.png