*函数的调用过程(栈桢)*
下面我们主要从栈的层面深入了解c语言中函数调用的过程。
下面我用一个简单的程序说明:
#include<stdio.h>
#include<stdlib.h>
int Add(int X, int Y)
{
int z = 0;
z = X + Y;
return z;
}
int main()
{
int a = 20;
int b = 10;
int ret = 0;
ret = Add(a, b);
printf("ret=%d\n", ret);
system("pause");
return 0;
在上面的程序里,主函数main里定义了3个局部变量,然后调用了Add函数。3个局部变量 都是在栈空间上存放的,当程序运行起来我们研究函数调用是用了哪些准备工作,准备工作做完后是如何传参的,参数传过去是如何使用参数的,参数使用完是如何销毁的,函数是如何返回的,接下来这整个过程我们通过汇编的角度来一步一步的看清楚。
下面我们进入调试功能来看程序的反汇编:
main函数汇编代码:
int main()
{
011E1810 push ebp
011E1811 mov ebp,esp
011E1813 sub esp,0E4h
011E1819 push ebx
011E181A push esi
011E181B push edi
011E181C lea edi,[ebp-0E4h]
011E1822 mov ecx,39h
011E1827 mov eax,0CCCCCCCCh
011E182C rep stos dword ptr es:[edi]
int a = 10;
011E182E mov dword ptr [ebp-4],14h
int b = 20;
011E1835 mov dword ptr [ebp-8],0Ah
int ret = 0;
011E183C mov dword ptr [ebp-0Ch],0
= Add(a, b);
011E1843 mov eax,dword ptr [ebp-8]
011E1846 push eax
011E1847 mov ecx,dword ptr [ebp-4]
011E184A push ecx
011E184B call _Add (011E110Eh)
011E1850 add esp,8
011E1853 mov dword ptr [ebp-0Ch],eax
printf("ret=%d\n", ret);
011E1856 mov eax,dword ptr [ebp-0Ch]
011E1859 push eax
011E185A push offset string "ret=%d\n" (011E7B30h)
011E185F call _printf (011E133Eh)
011E1864 add esp,8
system("pause");
011E1867 mov esi,esp
011E1869 push offset string "pause" (011E7B3Ch)
011E186E call dword ptr [__imp__system (011EB168h)]
system("pause");
011E1874 add esp,4
011E1877 cmp esi,esp
011E1879 call __RTC_CheckEsp (011E112Ch)
return 0;
011E187E xor eax,eax
}
011E1880 pop edi
011E1881 pop esi
011E1882 pop ebx
011E1883 add esp,0E4h
011E1889 cmp ebp,esp
011E188B call __RTC_CheckEsp (011E112Ch)
011E1890 mov esp,ebp
011E1892 pop ebp
011E1893 ret
Add函数汇编代码:
int Add(int X, int Y)
{
011E1700 push ebp
011E1701 mov ebp,esp
011E1703 sub esp,0CCh
011E1709 push ebx
011E170A push esi
011E170B push edi
011E170C lea edi,[ebp-0CCh]
011E1712 mov ecx,33h
011E1717 mov eax,0CCCCCCCCh
011E171C rep stos dword ptr es:[edi]
int z = 0;
011E171E mov dword ptr [ebp-4],0
z = X + Y;
011E1725 mov eax,dword ptr [ebp+8]
011E1728 add eax,dword ptr [ebp+0Ch]
011E172B mov dword ptr [ebp-4],eax
return z;
011E172E mov eax,dword ptr [z]
}
011E1731 pop edi
011E1732 pop esi
011E1733 pop ebx
011E1734 mov esp,ebp
011E1736 pop ebp
011E1737 ret
首先,我们在这里必须的先了解俩个寄存器:
ebp
———栈底的地址
esp
———栈顶的地址
每一个函数的调用都需要在栈上开辟空间,而开辟的空间都由ebp和esp维护
而且ebp和esp维护的是调用main函数的mainCRTStartup函数的空间。
接下来我们进去main函数痕迹汇编代码一步一步来了解函数调用的过程以及栈的创建和销毁:
int main()
{
011E1810 push ebp
push就是压栈,这句话就是把ebp压到栈顶,esp指向栈顶。其实结果如下图所示:
011E1811 mov ebp,esp
这段代码就是把esp的值给ebp,也就是ebp指向esp的位置;
效果如下图:
011E1813 sub esp,0E4h
这段代码就是esp减去0E4h,栈空间有高地址指向低地址。在这里实际就是向上开辟了0E4h大小的空间,这块空间是为main函数开辟的,esp此时指向栈顶,效果如图:
011E1819 push ebx
011E181A push esi
011E181B push edi
这三行代码,就是进行压栈操作,分别把ebx,esi,edi分别压进去,然后esp指向栈顶:
011E181C lea edi,[ebp-0E4h]
这段代码:ebp-0E4h就是刚才开辟的main函数的空间。lea就是加载的意思,这一行代码就是将main函数开辟的空间加载到edi里面。
011E1822 mov ecx,39h
011E1827 mov eax,0CCCCCCCCh
将39h给ecx,在把0cccccccch给eax,
011E182C rep stos dword ptr es:[edi]
这行代码的意思就是重复拷贝edi空间里面的内容,拷贝的内容,eax:
0cccccccch,拷贝次数ecx,39h次。
也就是把main函数开辟的空间地址全部初始化为0cccccccch。
……
当这4行代码全部运行完之后,查看内存可以看到从ebp-0e4h往下的57行全部被初始化为0ccccccccch。
int a = 10;
011E182E mov dword ptr [ebp-4],14h
int b = 20;
011E1835 mov dword ptr [ebp-8],0Ah
int ret = 0;
011E183C mov dword ptr [ebp-0Ch],0
这三行代码就是将main函数开辟的空间初始化:a=10,地址为ebp-4;
b=20,地址为ebp-8;ret=0,地址为ebp-0Ch;效果图如下:
011E183C mov dword ptr [ebp-0Ch],0
= Add(a, b);
011E1843 mov eax,dword ptr [ebp-8]
011E1846 push eax
011E1847 mov ecx,dword ptr [ebp-4]
011E184A push ecx
011E184B call _Add (011E110Eh)
011E1850 add esp,8
011E1853 mov dword ptr [ebp-0Ch],eax
上面这段代码意思就是:将ebp-8的值(20)给eax,在将eax从栈顶压入,此时esp指向eax;然后将ebp-4的值(10)的值给ecx,再将ecx从栈顶压入,此时esp指向eax;call调用函数,调用完之后回到最近一条指令即add的位置。此时相当于栈顶又压入了add函数的地址;具体如下图所示:
然后按f11进去Add函数:
int Add(int X, int Y)
{
011E1700 push ebp
011E1701 mov ebp,esp
011E1703 sub esp,0CCh
011E1709 push ebx
011E170A push esi
011E170B push edi
011E170C lea edi,[ebp-0CCh]
011E1712 mov ecx,33h
011E1717 mov eax,0CCCCCCCCh
011E171C rep stos dword ptr es:[edi]
int z = 0;
011E171E mov dword ptr [ebp-4],0
z = X + Y;
011E1725 mov eax,dword ptr [ebp+8]
011E1728 add eax,dword ptr [ebp+0Ch]
011E172B mov dword ptr [ebp-4],eax
return z;
011E172E mov eax,dword ptr [z]
}
这里Add函数的调用同main函数调用过程相同,运行上端代码后得到结果如图示:
栈的销毁:
011E1731 pop edi
011E1732 pop esi
011E1733 pop ebx
pop就相当于出栈的意思,esp此时指向ebx下面的空间,这三个地址相当于被回收;
011E1734 mov esp,ebp
011E1736 pop ebp
011E1737 ret
这里把ebp的值给esp,然后再将ebp出栈,这里的ebp是main函数的ebp,ret指令要返回值,首先把栈顶call执行下一条指令的地址出栈,然后紧接着跳到下面这一行的地址。这也就是为什么把之前的地址保存起来,起一定的返回作用。
011E1850 add esp,8
esp+8直接把定义的形参跳了过去,到这一步的时候,Add栈帧已经被销毁了。
011E1853 mov dword ptr [ebp-0Ch],eax
eax里存放的是Add函数里的的值,把eax的值放入ebp-0Ch,ret就把z的值返回了。
在栈帧销毁的过程中返回值是通过寄存器返回的。