函数调用栈是一块连续的用来保存函数运行状态的内存区域,调用函数(caller)和被调用函数(callee)根据调用关系堆叠起来。栈在内存区域中从高地址向低地址生长。 每个函数在栈上都有自己的栈帧,用来存放局部变量、函数参数等信息。当caller调用callee时,callee对应的栈帧就会被开辟,当调用结束返回caller时,callee对应的栈帧就会被销毁。
当函数调用发生时,首先需要保存caller的状态,以便函数调用结束后进行恢复,然后创建callee的状态。
当机器为32位:
在32位程序中,寄存器ebp指向栈帧的底部,用来存储当前栈帧的基址,在函数运行过程中不变,可以用来索引函数参数和局部变量的位置。寄存器esp指向栈帧的顶部,当栈生长时,esp的值减少(向低地址生长)。寄存器eip用于存储下一条指令的地址。
参数传递方式为:从右向左压栈
当机器为64位:
在64位程序中,三个寄存器分别为rbp、rsp和rip。如果是64位程序,前6个参数分别通过rdi、rsi、rdx、rcx、r8、r9进行传递,剩余参数从后向前压栈。如果callee不需要参数,则这一步骤省略。
对于64位程序,如果只有2个参数:
mov rsi, arg2
mov rdi, arg1
当存在8个参数:
push arg8
push arg7
mov r9, arg6
mov r8, arg5
mov rcx, arg4
mov rdx, arg3
mov rsi, arg2
mov rdi, arg1
-
将caller调用callee后的下一条指令的地址压入栈中,作为callee的返回地址,这样,当函数返回后可以正常执行接下来的指令。
-
将当前ebp寄存器的值压入栈中,这是caller栈帧的基址,将ebp更新为当前的esp。
-
将callee的局部变量压入栈中。
-
函数调用结束后,就是上面过程的逆过程,callee栈帧中数据会出栈,恢复到caller栈帧状态。
上面的第1步由caller完成,第2步在caller执行call指令时完成,第3、4步由callee完成