函数调用过程&栈帧&调用约定

函数调用过程 Procedure

参数传递

考虑函数调用:func(1, 2)
需要把1和2这两个参数进行传递,这里的参数传递可以通过两种方式:
1.参数入栈(内存)
2.参数传递到寄存器
大多数情况下,也是C\C++的默认形式是通过栈进行传递,因为虽然寄存器传递方式快但是寄存器数量有限
参数压入栈中(内存),CS:IP指向下一条指令地址需要进行跳转到函数入口就需要进行原地址的保存,这也是通过压入栈中来解决
注意这里,由于传递哪些参数调用者是知道的,所以参数压入必须由调用者来进行。
通过call指令来跳转同时把原地址压栈保存
跳转后,进入到函数的入口地址,必须做的两件事情:

  1. 将BP寄存器(栈底地址也叫栈基址)设定为SP地址,因为SP始终指向栈顶位置当进行函数调用时,需要建立一个新的栈帧(Stack Frame)这时将基址设定为栈顶即可,但是同时原始的栈基址必须入栈进行保存,所以将先进行push压入BP内容,这是通过sub指令进行
  2. 预留栈空间给局部变量,也就是将SP内容进行设定,设定的值由编译器根据局部变量,临时变量数量自动计算

这两件事情做完后,进行函数体的执行,然后进入到函数返回阶段
函数的返回需要进行2件事情:

  1. 将SP恢复为BP内容,因为在函数进入之初,SP给了BP,然后进行预留空间导致SP在函数退出阶段,和BP中间差了预留空间的长度。所以为了进行“平衡”必须恢复到函数进入之初
  2. 恢复BP内容为上一个栈帧基址,由于在进入之初push了BP所以只需要pop即可

最后进行ret,弹出返回地址并置cs:ip

这时已经返回到调用者的位置,由于之前参数进行了压栈,所以需要把参数都弹出,也就是把SP加上参数占用的空间即可

栈帧 Stack Frame


维护了一个函数调用的信息,一个函数的栈帧并不包含传入的参数,传入的参数在上一个栈帧后面

调用约定 Call Convention

有3种调用约定:

调用约定 参数入栈顺序 参数清理负责 备注
__cdecl 从右向左 调用者负责,调用者负责就是调用者需要把SP地址退回到参数之前,由于每次这种约定每次调用的地方都会多一条指令,所以会导致代码膨胀 C\C++默认调用约定,由于调用者负责堆栈平衡所以支持可变参数…
__stdcall 从右向左 被调用者负责堆栈平衡,也就是被调用函数的最后ret指令需要加上一个数值指定SP回退的地址空间,由于在被调用者内,所以不会导致额外代码空间的开销 由于被调用者负责清理,所以不能支持可变参数,Windows API大多都是该调用约定
__fastcall 前两个参数利用寄存器进行参数传递,后面参数利用栈 被调用者 由于借助寄存器,所以速度较快

什么是堆栈平衡

堆栈平衡指能正确维护SP寄存器也就是栈顶的位置,例如在函数即将退出之际,由于参数的传递利用了栈,所以函数调用完成后,必须把参数占用的空间给“还”回去。也就是SP指针加上一个数值,到达第一个参数之前的内存位置。
不同的函数调用约定,进行堆栈平衡的角色时不同的,参考上表

函数返回值

利用AX寄存器进行返回值传递

可变参数为什么必须是__cdecl

由于可变参数压入参数的个数不固定,如果让被调用者负责进行堆栈平衡,由于被调用者并不知道压入参数的个数,所以无法进行堆栈平衡。因为ret指令后面得使用一个常数,由于是常数所以必须事先确定参数个数
而cdecl是调用者负责平衡,所以常数可以任意,只需放在函数调用返回后即可

实践

__fastcall

int __fastcall Add(int a, int b, int d)
{
	int c = a + b;
	return c;
}

汇编代码:

push        ebp  
mov         ebp,esp  
sub         esp,0E4h
...
mov         esp,ebp  
pop         ebp  
ret         4 

如上,由于前两个参数通过寄存器传递,所以ret只需加上4个字节即可完成堆栈平衡,这里是被调用者进行平衡
再看调用的地方:

push        4  
mov         edx,3  
mov         ecx,2  
call        Add (02B1361h)  
mov         dword ptr [a],eax  

如上push 4把参数(常量参数)入栈,前面两个参数使用寄存器传递,从右向左

__stdcall

int __fastcall Add(int a, int b, int d)
{
	int c = a + b;
	return c;
}

由于fastcall和stdcall都是被调用这负责推展平衡,所以汇编代码类似

__cdecl

int __cdecl Add(int a, int b, int d)
{
	int c = a + b;
	return c;
}
pop         edi  
pop         esi  
pop         ebx  
mov         esp,ebp  
pop         ebp  
ret

如上,函数返回的ret指令不加参数,直接返回

push        4  
push        3  
push        2  
call        Add (0117136Bh)  
add         esp,0Ch  
mov         dword ptr [a],eax  

调用者这里使用add指令来进行堆栈平衡

参考

https://zhuanlan.zhihu.com/p/27339191 非常详细的调用过程分析
https://blog.csdn.net/luoweifu/article/details/52425733
https://www.cnblogs.com/33debug/p/6773059.html
https://www.jianshu.com/p/b666213cdd8a

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章