前置准备
pop:
Pop指令的作用是弹栈,将栈顶的数据弹出到寄存器,然后栈顶指针向下移动一个单位。
具体来说:如pop rax,作用就是mov rax[rsp];add rsp 8;
push:
Push指令的作用就是压栈,将栈顶指针向上移动一个单位的距离,然后将一个寄存器的值存放在栈顶,
具体来说:如pushrax,其实际效果就是:sub rsp 8; mov [rsp] rax;
jmp:
立即跳转,不涉及函数调用,用于循环、ifelse这种场合
具体来说:如jmp 1234h,效果就是:mov rip 1234h;
call:
函数调用,需要保存返回地址。
具体来说:如call1234h,效果就是:push rip;mov rip 1234h;
Ret:
用于函数返回
具体来说,就是pop rip。
函数调用流程
从一个实际例子出发
main调用func_b, func_b调用func_a。
我们从main函数开始,逐步分析栈帧变化。
当运行到call func b时main函数的栈帧。
Rbp指向栈顶,rsp指向栈底
这段栈帧存放了一些main的局部变量。
main函数要调用func b,main只需要call func b
也就是push rip; mov rip func b;
那么此时跳转到funcb继续执行,funcb直接执行主逻辑吗?
显然不是的,被调用函数还需要维护栈帧。
具体来说,需要以下几步:
push rbp;将调用函数的栈底指针保存,
mov rbp rsp;将栈底指针指向现在的栈顶。
sub rsp xxx;开辟被调用函数的栈帧,此时上一步的rbp就指向栈帧的底
func b执行完维护栈帧操作后的栈布局,
所谓栈帧的维护就是维护rbp和rsp两个指针。
Rsp永远指向栈顶。
Rbp用来定位局部变量。
现在,func_b要调用func_a,其调用流程与main函数调用func_b基本一致
不同处在于返回地址、rbp和rsp指向的地址,以及开辟的空间不同
在这里,我们学习一个新的汇编指令:leave。
leave指令:
作用是维护栈帧,通常出现在函数的结尾,与ret连用
其实际作用为:mov rsp rbp;pop rbp;
即:将栈顶指针指向栈帧的底部,然后在栈中弹出新的栈底指针。
在一个函数执行结束返回时,会执行leave;ret;
实际效果就是:movrsp rbp;pop rbp;pop eip;
此时我们观察程序执行至func a时的栈帧。
func a执行完毕返回后,栈布局如图
可以与之前的func b未调用func a前的栈帧对比
一模一样,说明已经恢复了栈帧。
唯一不同之处在于此时程序的rip已经指向了c=1
后面一条指令,说明func_a已经执行完毕。
以此类推,func b执行完毕返回后,栈布局如图
在这之后,main函数继续执行,直到结束。
至此,函数的调用返回执行流程结束。
总结:
调用函数:只需要将rip压栈,即pushrip,然后讲rip赋值为被调用函数的起始地址,这一操作被隐性的内置在call指令中。
被调用函数:push rbp;mov rbp rsp;sub rsp 0xxxx。即保存调用函数的rbp指针,将自己的rbp指针指向栈顶,然后开辟栈空间给自己用,此时rbp就变成了被调用函数的栈底。
函数返回:leave;ret;翻译过来就是:mov rsp rbp;pop rbp;pop rip;即恢复栈帧,返回调用函数的返回地址
参数传递方法——调用约定
返回值:一般来说,一个函数的返回值会存储到RAX寄存器
X86-64函数的调用约定为
从左至右参数-次传递给rdi,rsi,rdx,rcx,r8,r9
如果一个函数的参数多于6个,!则从右至左压入栈中传递
系统调用
syscall指令:
用于调用系统函数,调用时需要指明系统调用号
系统调用号存在rax寄存器中,然后布置好参数,执行syscall即可
示例:调用read(0,buf,size);
mov rax ;read's syscall number
mov rdi 0;first arg
mov rsi buf;second arg
mov rdx size;third ard
syscall;execute read(0 buf,size);