调用函数主要关注三个方面分别是函数名,返回值和参数列表,我接下来将会深入底层讲解调用函数的过程。
调用函数的过程主要有四方面,①函数参数代入,②函数栈帧开辟,③函数返回值,④函数栈帧回退
首先来看一段简单的c文件代码,和它的汇编码,只需简单浏览即可:
源码:
int fun1(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int a = fun1(10, 20);
return 0;
}
汇编码:
main函数
int main()
{
00C31410 push ebp
00C31411 mov ebp,esp
00C31413 sub esp,0CCh
00C31419 push ebx
00C3141A push esi
00C3141B push edi
00C3141C lea edi,[ebp-0CCh]
00C31422 mov ecx,33h
00C31427 mov eax,0CCCCCCCCh
00C3142C rep stos dword ptr es:[edi]
int a = fun1(10, 20);
00C3142E push 14h
00C31430 push 0Ah
00C31432 call fun1 (0C31127h)
00C31437 add esp,8
00C3143A mov dword ptr [a],eax
return 0;
00C3143D xor eax,eax
}
00C3143F pop edi
00C31440 pop esi
00C31441 pop ebx
00C31442 add esp,0CCh
00C31448 cmp ebp,esp
00C3144A call __RTC_CheckEsp (0C3113Bh)
00C3144F mov esp,ebp
00C31451 pop ebp
00C31452 ret
fun1函数
int fun1(int a, int b)
{
009813D0 push ebp
009813D1 mov ebp,esp
009813D3 sub esp,0CCh
009813D9 push ebx
009813DA push esi
009813DB push edi
009813DC lea edi,[ebp-0CCh]
009813E2 mov ecx,33h
009813E7 mov eax,0CCCCCCCCh
009813EC rep stos dword ptr es:[edi]
int c = a + b;
009813EE mov eax,dword ptr [a]
009813F1 add eax,dword ptr [b]
009813F4 mov dword ptr [c],eax
return c;
009813F7 mov eax,dword ptr [c]
}
009813FA pop edi
009813FB pop esi
009813FC pop ebx
009813FD mov esp,ebp
009813FF pop ebp
00981400 ret
首先知道ebp为栈底寄存器,esp为栈顶寄存器。push为操作,操作方式为在esp中的栈顶存放数据,栈顶上移。
在查看fun函数参数如何代入之前,我们先看一下main的栈顶和栈底
(1)函数参数代入
两次push后,参数就被放在了main函数的栈顶,且入栈为从右往左,效果如下:
(2)fun函数栈帧开辟
可以看到,call就相当于调用函数,这里重新设置了栈底ebp和栈顶esp,过程如下:
查看fun函数的开辟栈帧的过程会发现它与main函数的开辟及其类似,其实,main函数的下面也有主函数参数,也就是可以把main函数也看做为一个普通函数。
(3)函数返回
利用寄存器带回,将寄存器的值写入接收返回值的常量
(4)栈帧回退
将fun的栈顶指向fun的栈底:
mian函数中:esp移动8位,消除参数
现在就回到了main函数的栈顶。
(5)其他问题
①因为参数大小不同,8字节及以上的参数采用的是提前在栈顶开辟内存,以保存大字节的参数。
②返回8字节及以上的返回值也是采用提前开辟内存的方法。
③有三种不同的约定调用方式,分别为__cedel、__stdcall、__fastcall,这三种方式有一些细节的不同,但是思想相同,本文讲的是__cedel方式。