函数调用过程分析
参考:轩辕之风——从0开始学逆向第7天
函数调用约定
定义
在计算机科学中,调用约定是一种定义子过程从调用处接受参数以及返回结果的方法的约定。
不同调用约定的区别在于: 参数和返回值放置的位置、参数传递的顺序、调用前设置和调用后清理的工作,在调用者和被调用者之间如何分配,被调用者可以直接使用哪一个寄存器有时也包括在内。——维基百科
简单理解:main函数内调用func1函数,他们之间的参数、返回值如何传递,进行提前约定。
3种约定类型
不同点:
- 参数使用栈传递 OR 寄存器传递
- 调用者 OR 被调用者负责释放参数空间
使用在线反编译工具分析调用过程。https://godbolt.org/
选择x86 msvc v19.latest
编译方式。
如何约定调用方式?
在被调用函数之前声明约定类型关键字即可。
int 调用方式 func(int a,int b){
int sum = a+b;
return sum;
}
int main(){
int a=1,b=2;
int c = func(a,b);
return 0;
}
__cdecl调用方式
func子函数
main主函数
参数使用栈传递,调用者负责释放参数空间。
__stdcall调用方式
参数使用栈传递,被调用者负责释放参数空间。
在 __stdcall 调用约定中,子函数通过 __cdecl 调用约定返回 8,这相当于在 __cdecl 调用约定中主函数的
add esp, 8 指令
__fastcall调用方式
int __fastcall func(int a,int b,int c){
int sum = a+b+c;
return sum;
}
int main(){
int a=1,b=2,c=3;
int d = func(a,b,c);
return 0;
}
主函数main,这里func(a,b,c)的传参,前两个参数ab使用寄存器,而第三个参数c使用栈传递。
子函数func
参数使用寄存器(可能含栈)传递,被调用者负责释放参数空间。可以预见,这里使用栈传递的参数依然符合从右到左的传参方式。
通过以上__fastcall的汇编调用还可以看出。参数使用栈传递时,由调用函数在栈中开辟一段新空间,用于储存待传递的参数,子函数从这个主函数栈里面取值。而前两个参数使用寄存器传递时,又在子函数栈中开辟新空间,从寄存器中取值放到栈中,需要的时候再从子函数栈中取。
practice
1、请分析下面这段代码使用fastcall调用约定时是如何工作的:
int fun1(int a, int b, int c, int d) {
return a*a + b*b + 2*a*b + c + d;
}
int main() {
int c = fun1(3, 4, 5, 6);
return 0;
}
- fastcall调用,fun1(3, 4) 参数放入寄存器,fun1(5, 6) 放入push入栈
- 进入call调用,ip入栈(保存函数调用前位置),子函数申请一段内存存放中间运算结果
- 最后
return 申请的空间大小
进行释放空间
小结
在汇编语言中,函数调用通常包括两个主要步骤:
- 参数传递
- 函数调用
参数传递通常通过 push指令将参数放入栈中,函数调用通过call指令跳转到函数的代码。
从 push语句开始,可以将这个过程视为进入了函数调用阶段。这是因为 push指令开始了参数的传递,这是函数调用的一部分。然后,call指令使程序跳转到函数的代码,这是函数调用的另一部分。
只有当call指令执行后,程序才会真正进入到函数的代码中(分两步)。在call指令执行之前,程序还在主函数的上下文中,此时已经开始了参数传递。