今天我们来介绍一下函数的堆栈调用过程,首先,我们以一个简单的例子进行说明:
#include<stdio.h>
int Sum(int a,int b)
{
int temp=0;
temp=a+b;
return temp;
}
int main()
{
int a=10;
int b=20;
int ret=0;
ret=Sum(a,b);
printf("ret=%d\n",ret);
return 0;
}
将上面代码转到反汇编
Sum函数:
int Sum(int a,int b)
{
008E13D0 push ebp //将ebp入栈
008E13D1 mov ebp,esp //将esp赋给ebp
008E13D3 sub esp,0CCh //esp减等于0cch,即给Sum函数开辟0CCh大的栈帧空间
008E13D9 push ebx // 将ebx入栈
008E13DA push esi //将esi入栈
008E13DB push edi //将edi入栈
008E13DC lea edi,[ebp-0CCh] //mov移值,lea移地址,将ebp-0cch放到edi里
008E13E2 mov ecx,33h 将33h放到ecx中,ecx是计数器
008E13E7 mov eax,0CCCCCCCCh //将0 CCCCCCCC h放到eax中,eax是累加器
008E13EC rep stos dword ptr es:[edi] // 循环拷贝,从edi开始拷贝数据,向高地址部分进行字节拷贝,每次拷贝4个字节,拷贝eax的内容,拷贝33h次,上面这几步给开辟的空间初始化,和main函数中的过程是一样的
int temp=0;
008E13EE mov dword ptr [ebp-4],0 //
temp=a+b;
008E13F5 mov eax,dword ptr [ebp+8] //把ebp+8的值即a的值放在eax寄存器里
008E13F8 add eax,dword ptr [ebp+0Ch] //把ebp+12的值即b的值加上eax寄存器里的值的和放在eax中
008E13FB mov dword ptr [ebp-4],eax //将eax的值放到ebp-4即temp里
return temp;
008E13FE mov eax,dword ptr [ebp-4] //将ebp-4的值放到eax中,通过eax寄存器带出返回值
}
00E21401 pop edi //出栈
00E21402 pop esi
00E21403 pop ebx
00E21404 mov esp,ebp //将ebp赋给esp,相当于把esp拉到ebp的位置(回退栈帧),注意回退栈帧时并不清0
00E21406 pop ebp //出栈,esp向下(高地址)挪,并把出栈的元素(main函数的栈底指针的值)赋给ebp,即ebp重新回到main函数的栈底
00E21407 ret //出栈,出栈的元素赋给CPU的PC寄存器(下一行指令的地址)
main函数:
int main()
{
008E1420 push ebp
008E1421 mov ebp,esp
008E1423 sub esp,0E4h
008E1429 push ebx
008E142A push esi
008E142B push edi
008E142C lea edi,[ebp-0E4h]
008E1432 mov ecx,39h
008E1437 mov eax,0CCCCCCCCh
008E143C rep stos dword ptr es:[edi]
int a=10;
008E143E mov dword ptr [ebp-4],0Ah //将10存入ebp-4所表示的起始地址的4个字节里边,word表示2个字节,dword表示4个字节
int b=20;
008E1445 mov dword ptr [ebp-8],14h //将20存入ebp-8所表示的起始地址的4个字节里边
int ret=0;
008E144C mov dword ptr [ebp-0Ch],0 //将0存入ebp-0ch所表示的起始地址的4个字节里边
ret=Sum(a,b);
008E1453 mov eax,dword ptr [ebp-8] //从ebp-8即b的内存中拿4个字节放在eax寄存器
008E1456 push eax //将eax入栈,即将b的值入栈
008E1457 mov ecx,dword ptr [ebp-4] //从ebp-4即a的内存中拿4个字节放在ecx寄存器
008E145A push ecx //将ecx入栈,即将a的值入栈
008E145B call Sum (08E11C7h) //将下一行指令的地址入栈,并跳转到Sum函数的栈帧上
008E1460 add esp,8 //回退形参变量所占的内存
008E1463 mov dword ptr [ebp-0Ch],eax //将eax寄存器带出来的值放在ebp-0Ch里即ret中
printf("ret=%d\n",ret);
008E1466 mov esi,esp
008E1468 mov eax,dword ptr [ebp-0Ch]
008E146B push eax
008E146C push 8E5858h
008E1471 call dword ptr ds:[8E92BCh]
008E1477 add esp,8
008E147A cmp esi,esp
008E147C call __RTC_CheckEsp (08E1136h)
return 0;
008E1481 xor eax,eax
}
00E21483 pop edi
00E21484 pop esi
00E21485 pop ebx
00E21486 add esp,0E4h
00E2148C cmp ebp,esp
00E2148E call __RTC_CheckEsp (0E21136h)
00E21493 mov esp,ebp
00E21495 pop ebp
00E21496 ret
通过上述反汇编代码,我们可以看到,无论是Sum函数还是main函数,其反汇编代码前面都有一部分类似于这样的代码:
其对应的解释为:
每一个函数开始调用时的汇编指令固定做的事,我们可以总结为以下三步:
(1)把主调方函数的栈底地址入栈,然后让ebp指针指向当前函数的栈底;
(2)通过esp的减等于操作,给被调用函数开辟栈帧;
(3)把esp与ebp之间的所有栈内存全部初始化为0XCCCCCCCC
上述过程的描述如下图:
一、探究问题及其答案
1.形参开辟内存吗?是由谁来开辟的?调用方还是被调用方?
答:形参开辟内存;是由调用方开辟的
2.形参的入栈顺序?
答:形参按从右向左的顺序入栈;实参与形参匹配类型是从左向右匹配的
3.被调函数的返回值由谁带出?
答:若0<返回值<4个字节,由eax寄存器带出
若4<返回值<8个字节,由eax和edx寄存器带出
若返回值>8个字节,用临时量带出,临时量在内存中存储
4.被调用方回退后怎么会到main函数栈帧上?
答:调用方栈底指针的地址保存到被调用方栈底指针
5.函数调用完成后,如何知道要继续进行下一行指令而不是从头开始执行?
答:函数调用时,将下一行指令的地址压栈,在清理被调用方函数栈帧的过程中又将其放在了下一行指令寄存器PC中,所以会回到调用方的下一行指令处继续向下执行。
二、函数开栈、清栈过程
开栈
1.形参初始化:压入实参,将对应的实参值从右向左依次压入栈顶
2.压入下一行指令地址。原因:当被调函数清栈后能沿着调用方继续向下执行
3.压入调用方的栈底指针寄存器的值。原因同上退栈后能回到调用方,此时还处在调用方栈帧上
4.移动ebp到被调用方栈底,跳转到被调用方函数栈帧
5.为被调用方开辟活动空间,并初始化为cccc cccc
清栈过程与开栈正好相反。
三、常见的简单汇编指令
在真正体会函数堆栈调用时,必须要看懂反汇编代码,下面列出了一些常见的反汇编指令:
mov, dword ptr[a],0ah
mov, 0ah, dword, ptr[a]
上述两行会反汇编语句的意思都是 :把a放在[a]对应的内存块上放4个字节
mov, dword ptr[ebp-4] //移值的指令:ebp栈底寄存器
lea, eax, [ebp-4] //移地址的指令,eax累加寄存器
push 0ah //压栈
pop eax //====>> eax=pop();将栈顶元素取出放入寄存器里
add eax, oah //====>> eax+=0a
sub eax, 0ah //====>>eax-=0a
ret 返回值指令
四、常用寄存器
eax:"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器;
ebx:"基地址"(base)寄存器, 在内存寻址时存放基地址;
ecx:计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器;
edx:总是被用来放整数除法产生的余数;
ebp:栈底指针寄存器,存放调用方栈底指针的地址,main()的调用方是mainCRTStartup
esp:栈顶指针寄存器;
pc:下一行指令寄存器;
eip:寄存器存放下一个CPU指令存放的内存地址,当CPU执行完当前的指令后,从eip寄存器中读取下一条指令的内存地址,然后继续执行。
call 调用函数,潜在的还做了:
(1)压入下一行指令的地址
(2)跳到被调用方函数
不返回局部比变量的地址,清栈后数据还存在,只不过是告诉系统该区域可被再次分配。
函数的调用约定:
1.C的标准调用约定:__cdecl (所有C和C++体系中的全局函数(普通函数)默认__cedcl调用约定)
2.windows标准调用约定:__stdcall
3.快速调用约定:__fastcall
4.成员方法的调用约定:__thiscall