函数的堆栈调用

今天我们来介绍一下函数的堆栈调用过程,首先,我们以一个简单的例子进行说明:

#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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值