函数调用过程

今天突然看到有人私信我说一直没写函数调用过程(栈帧的形成和销毁过程)这篇博文,赶紧补上。
刚看的栈帧内容时,我很迷惑,我觉得栈帧创建和销毁很麻烦,几句话根本说不完,而且我好像描述不清楚他的过程,所以在博文里面遇到函数调用我就规避了。现在再写栈帧调用过程,我觉得其实这个过程没有那么困难(不过还是有些抽象,毕竟计算机底层怎么运行我们也不是很明白)。
栈帧的创建的销毁过程例子代码:

int Add(int x,int y)
{
    int sum = 0;
    sum = x + y;
    return sum;
}

int main ()
{
    int a = 10;
    int b = 12;
    int ret = 0;
    ret = Add(a,b);
    return 0;
}

今天主要用汇编代码去讲述这个过程,首先介绍几个寄存器和简单的汇编指令的意思。
先看几个函数调用过程涉及到的寄存器:
(1)esp:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
(2)ebp:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。
(3)eax 是”累加器”(accumulator), 它是很多加法乘法指令的缺省寄存器。
(4)ebx 是”基地址”(base)寄存器, 在内存寻址时存放基地址。
(5)ecx 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。
(6)edx 则总是被用来放整数除法产生的余数。
(7)esi/edi分别叫做”源/目标索引寄存器”(source/destination index),因为在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串.
在32位平台上,ESP每次减少4字节。
再看几条简单的汇编指令:
mov :数据传送指令,也是最基本的编程指令,用于将一个数据从源地址传送到目标地址(寄存器间的数据传送本质上也是一样的)
sub:减法指令
lea:取偏移地址
push:实现压入操作的指令是PUSH指令
pop:实现弹出操作的指令
call:用于保存当前指令的下一条指令并跳转到目标函数。
这些指令当然能看懂最好,可以让你很深刻的理解函数调用过程,不能看懂就只能通过我的描述去理解了。
进行分析之前,先来了解下内存地址空间的分布:
进程地址空间分布
栈空间是向低地址增长的,主要是用来保存函数栈帧。 栈空间的大小很有限,仅有区区几MB大小
汇编代码实现:
main函数汇编代码:

int main ()
{
011B26E0  push        ebp  
011B26E1  mov         ebp,esp 
011B26E3  sub         esp,0E4h 
011B26E9  push        ebx  
011B26EA  push        esi  
011B26EB  push        edi  
011B26EC  lea         edi,[ebp-0E4h] 
011B26F2  mov         ecx,39h 
011B26F7  mov         eax,0CCCCCCCCh 
011B26FC  rep stos    dword ptr es:[edi] 
    int a = 10;
011B26FE  mov         dword ptr [a],0Ah 
    int b = 12;
011B2705  mov         dword ptr [b],0Ch 
    int ret = 0;
011B270C  mov         dword ptr [ret],0 
    ret = Add(a,b);
011B2713  mov         eax,dword ptr [b] 
011B2716  push        eax  
011B2717  mov         ecx,dword ptr [a] 
011B271A  push        ecx  
011B271B  call        @ILT+640(_Add) (11B1285h) 
011B2720  add         esp,8 
011B2723  mov         dword ptr [ret],eax 
    return 0;
011B2726  xor         eax,eax 
}
011B2728  pop         edi  
011B2729  pop         esi  
011B272A  pop         ebx  
011B272B  add         esp,0E4h 
011B2731  cmp         ebp,esp 
011B2733  call        @ILT+450(__RTC_CheckEsp) (11B11C7h) 
011B2738  mov         esp,ebp 
011B273A  pop         ebp  
011B273B  ret              

Add函数汇编代码:

int Add(int x,int y)
{
011B26A0  push        ebp  
011B26A1  mov         ebp,esp 
011B26A3  sub         esp,0CCh 
011B26A9  push        ebx  
011B26AA  push        esi  
011B26AB  push        edi  
011B26AC  lea         edi,[ebp-0CCh] 
011B26B2  mov         ecx,33h 
011B26B7  mov         eax,0CCCCCCCCh 
011B26BC  rep stos    dword ptr es:[edi] 
    int sum = 0;
011B26BE  mov         dword ptr [sum],0 
    sum = x + y;
011B26C5  mov         eax,dword ptr [x] 
011B26C8  add         eax,dword ptr [y] 
011B26CB  mov         dword ptr [sum],eax 
    return sum;
011B26CE  mov         eax,dword ptr [sum] 
}
011B26D1  pop         edi  
011B26D2  pop         esi  
011B26D3  pop         ebx  
011B26D4  mov         esp,ebp 
011B26D6  pop         ebp  
011B26D7  ret              

。 下面图中详细描述了调用过程地址变化(此处所有地址是取自32位windows系统vs编辑器下的调试过程。):
函数调用过程
过程描述:
1、参数拷贝(参数实例化)。
2、保存当前指令的下一条指令,并跳转到被调函数。
这些操作均在main函数中进行。

接下来是调用Add函数并执行的一些操作,包括:
1、移动ebp、esp形成新的栈帧结构。
2、压栈(push)形成临时变量并执行相关操作。
3、return一个值。
这些操作在Add函数中进行。

被调函数完成相关操作后需返回到原函数中执行下一条指令,操作如下:
1、出栈(pop)。
2、回复main函数的栈帧结构。(pop )
3、返回main函数
这些操作也在Add函数中进行。 至此,在main函数中调用Add函数的整个过程已经完成。
总结起来整个过程就三步:
1)根据调用的函数名找到函数入口;
2)在栈中审请调用函数中的参数及函数体内定义的变量的内存空间
3)函数执行完后,释放函数在栈中的审请的参数和变量的空间,最后返回值(如果有的话)
如果你学了微机原理,你会想到cpu中断处理过程,是的,函数调用过程和中断处理过程一模一样。

函数调用约定:
这里再补充一下各种调用规定的基本内容。
_stdcall调用约定

所有参数按照从右到左压入堆栈,由被调用的子程序清理堆栈

_cdecl调用约定(The C default calling convention,C调用规定)

参数也是从右到左压入堆栈,但由调用者清理堆栈。

_fastcall调用约定

顾名思义,_fastcall的目的主要是为了更快的调用函数。它主要依靠寄存器传递参数,剩下的参数依然按照从右到左的顺序压入堆栈,并由被调用的子程序清理堆栈。

本篇博文是按调用约定__stdcall 调用函数。

  • 19
    点赞
  • 65
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
函数调用过程是程序中常见的一种操作,它通常涉及到参数传递、栈帧的建立与销毁、返回值的传递等多个方面。从汇编的角度来看,函数调用过程可以分为以下几个步骤: 1. 将函数的参数压入栈中。在调用函数时,需要将函数所需的参数传递给它。这些参数通常以一定的顺序压入栈中,以便在函数内部使用。在 x86 架构中,参数的传递是通过将参数压入栈顶实现的。 2. 调用函数函数调用的指令通常是 CALL 指令。在调用函数前,需要将函数的入口地址压入栈中,以便在函数执行完毕后返回到调用位置。CALL 指令会将当前的程序计数器(PC)压入栈中,并将函数的入口地址作为新的 PC。 3. 建立栈帧。在函数被调用时,需要为函数建立一个独立的栈帧,以便在函数内部使用局部变量和临时变量。栈帧通常包括以下几个部分:返回地址、旧的基址指针、局部变量和临时变量。在 x86 架构中,栈帧的建立是通过将 ESP 寄存器减去一个固定的值实现的。 4. 执行函数。在函数被调用后,CPU 会跳转到函数的入口地址并开始执行函数函数内部可以通过栈中的参数和局部变量完成相应的计算和操作。 5. 返回值传递。在函数执行完毕后,需要将函数的返回值传递给调用者。在 x86 架构中,函数的返回值通常通过 EAX 寄存器传递。 6. 销毁栈帧。在函数执行完毕后,需要将栈帧销毁,以便释放栈空间。栈帧的销毁通常是通过将 ESP 寄存器还原到旧的基址指针处实现的。 7. 返回到调用位置。在函数执行完毕后,需要返回到函数被调用的位置。在 x86 架构中,返回指令通常是 RET 指令。RET 指令会将栈顶的返回地址弹出,并将其作为新的 PC。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙跃十二

写的不错,给点鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值