回顾一下
pop指令作用是弹栈,将栈顶的数据弹出到寄存器,然后栈顶指针向下移动一个单位。
具体来说:如pop rax,作用就是mov rax[rsp];add rsp 8
push指令的作用就是压栈,将栈顶指针向上移动一个单位的距离,然后将寄存器的值存放在栈顶。
具体来说:如push rax,其实际效果就是:sub rsp 8;mov [rsp] rax;
jmp:
立即跳转,不涉及函数调用,用于循环、ifelse这种场合。
具体来说:如jmp 1234h,效果就是:mov rip 1234h;
call:
函数调用,需要保存返回地址。
具体来说:如call 1234h,效果就是:push rip;mov rip 1234h;
Ret:
用于函数返回
具体来说,就是pop rip
函数调用流程
void func_a()
{
//do sth
return;
}
void func_b()
{
func_a();
int c=1;
return;
}
int main()
{
func_b();
int a=2;
return 0;
}
main调用func_b,func_b调用func_a。
一开始是
栈帧中存放一些main的局部变量
main函数要调用func_b,main只需要call func_b
也就是push rip;mov rip func_b;
此时跳转到func_b继续执行,func_b直接执行主逻辑吗?NO!!!
被调用函数还需要维护栈帧。
具体来说是:
push rbp; 将调用函数的栈底指针保存。
mov rbp rsp;将栈底指针指向现在的栈顶。
sub rsp xxx; 开辟被调用函数的栈帧,此时上一步的rbp就指向栈帧的底。
func_b 执行完维护栈帧操作后的栈布局
所谓栈帧的维护就是维护rbp和rsp两个指针
Rsp永远指向栈顶
Rbp用来定位局部变量
func_b要调用func_a,调用流程与main函数调用func_b基本一致
不同在于返回地址、rbp和rsp指向的地址,以及开辟的栈空间不同。
func_b调用完func_a后的栈布局
至此,函数调用已经完毕
func_a执行完毕,要返回了
如何维护栈帧呢?
leave指令
作用是维护栈帧,通常出现在函数的结尾,与ret连用。
其实作用为:mov rsp rbp;pop rbp;
即:将栈顶指针指向栈帧的底部,然后在栈中弹出新的栈底指针
在一个函数执行结束返回时,会执行leave;ret;
实际效果就是:mov rsp rbp;pop rbp;pop rip;
func_a执行完毕返回后,栈布局如图:
一模一样,说明已经恢复了栈帧。
唯一不同的是rip已经指向了c=1
后面一条指令,说明func_a已经执行完毕
以此类推,func_b执行完毕返回后,栈布局如图:
在这之后,main函数继续执行,直到结束。
函数的调用返回执行流程结束。
参数是怎么传递?--调用约定
返回值:一般来说,一个函数的返回值会存储到RAX寄存器。
x86--64函数的调用约定为:
从左至右参数依次传递给rdi、rsi、rcx、r8、r9。
如果一个函数参数多于6个,则从右至左压入栈中传递
32位都是用栈传递
系统调用
syscall指令:
用于调用系统函数,调用时需要指明系统调用号
系统调用号存储rax寄存器中,然后布置好参数,执行syscall即可。
x64:
mmap:系统层面的分配内存
alarm:定时
exit:退出
kill:杀死子进程
execv:执行一个新程序