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

函数调用流程

①从一个实际例子出发
②main调用func_b,func_b调用func_a
③从main函数开始,逐步分析栈帧变化
1.png

函数调用流程

①当运行到call func_b时main函数的栈帧
②RBP指向栈底,RSP指向栈顶
③这段栈帧存放了一些main函数的局部变量
④main函数要调用func_b,main只需要call func_b
⑤也就是push rip ;mov rip func_b;
2.png

函数调用流程

①那么此时跳转到func_b继续执行,func_b直接执行主逻辑吗?
②显然不是的,被调用函数func_b还需要维护栈帧。
③具体来说,需要以下几步:
push rbp;将调用函数的栈底指针保存。
mov rbp,rsp ;将栈底指针指向现在的栈顶
sub rsp,xxx;开辟被调用函数栈帧,此时上一步的rbp就是指向栈帧的底。

函数调用流程

①func_b执行完维护栈帧操作后的栈布局
②所谓栈帧的维护就是维护rbp和rsp两个指针
③RSP永远指向栈顶
④RBP用来定位局部变量
3.png

函数调用流程

①现在,func_b要调用func_a,其调用流程与main函数调用func_b基本一致。
②不同处在于返回地址、rbp和rsp指向的地址,以及开辟的栈空间的不同。

函数调用流程

①func_b调用完func_a后的栈布局
②至此,示例的函数调用已经完毕
③现在,func_a执行完毕,要返回了
4.png

函数调用流程

leave指令
①作用是维护栈帧,通常出现在函数的结尾,与ret连用
②其实际作用为mov rsp,rbp;pop rbp;
③将栈顶指针指向栈帧底部,然后在栈中弹出新的栈底指针

函数调用流程

①在一个函数执行结束返回时,会执行leave ; ret ;
②实际效果就是:mov rsp rbp; pop rbp ; pop eip ;
③此时我们观察程序执行到func_a时的栈帧。
5.png

函数调用流程

①func_a执行完毕返回后,栈布局如图:
②可以与之前的func_b未调用func_a前的栈帧对比
③一模一样,说明已经恢复了栈帧
④唯一不同之处此时的程序已经指向了c=1
⑤后面一条指令,说明func_a已经执行完毕

函数调用流程

①以此类推,func_b执行完毕返回后,栈布局如图:
②在这之后,main函数继续执行,直到结束
③至此,函数的调用返回执行流程结束。
6.png

函数调用流程

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

调用约定

1、返回值:一般来说,一个函数的返回值会存储到RAX寄存器中。
2、X86-64函数的调用约定为:
①从左到右参数一次传递给rdi,rsi,rdx,rcx,r8,r9。
②如果一个函数的参数多于6个,则从右到左压入栈中传递。

系统调用

syscall指令
①用于调用系统函数,调用时需要指明系统的调用号
②系统调用号存在rax寄存器中,然后不知好参数,执行syscall即可。

系统调用

7.png

示例:调用read(0,buf,size);

mov rax,0; read’s syscall number
mov rdi,0; first arg
mov rsi,buf; second arg
mov rdx,size; third arg
syscall; execute read(0,buf,size)