函数调用栈帧分析

分析函数调用栈帧变化过程,需要了解一些工具和几个汇编指令的学习。首先简单的介绍一下分析栈帧时必用的一些汇编指令。
工具
VS或VC中,调用堆栈窗口查看工具,反汇编窗口工具,监视窗口工具,内存窗口查看工具
汇编命令
mov指令:例:mov ax bx 表示把bx寄存器的值赋值给ax寄存器。
push指令:例:push ax 表示将寄存器ax中的数据送入栈中。
pop指令 :例:pop ax 表示从栈顶取出数据送入ax中。
esp指令:栈指针寄存器,其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的顶端。
ebp指令:基址指针寄存器,其内存放着一个指针,该指针永远指向系统栈最底下一个栈帧的底。
call指令:
第一步:先将call指令的下一条指令的CS和IP入栈(当然如果是段间转移就要将CS和IP入栈,如果是段内转移就只要将IP入栈)
第二步:就是操作与call对应的jmp指令
ret指令:和call指令相反操作,首先出栈,然后jmp到call指令压栈所存储的下一条指令的地址。
简单说,call指令实现的了从主函数(调用函数)到子函数(被调函数)的过程,而ret指令实现了从子函数回到主函数的过程。call指令和ret指令是实现的程序的模块化编程必不可少的条件。
rep stos指令:例

lea     edi,[ebp-0C0h] 
mov     ecx,30h 
mov     eax,0CCCCCCCCh 
rep stos dword ptr es:[edi]

rep指令的目的是重复其上面的指令.ecx寄存器的值是重复的次数.
stos指令的作用是将eax中的值拷贝到ES:EDI指向的地址.

为了在查看栈帧的变化过程,我写一个简单的函数调用测试程序来进行实践演示。

#include<stdio.h>
#include<stdlib.h>

int Add(int a, int b)
{
    int rete = 0;
    rete = a + b;
    return rete;
}

int main()
{
    int x = 4;
    int y = 5;
    int result = 0;
    result = Add(x, y);
    printf("%d\n", result);
    system("pause");
    return 0;
}

这里写图片描述
由上图可以看出函数的调用顺序是:系统调用->mainCRTSartup()->_tmainCRTStartup()函数->main()函数->其它函数调用。

如图:
这里写图片描述
查看反汇编窗口:
这里写图片描述
查看此时的esp与ebp的值,此时esp和ebp的值保存的是main()函数之前的函数_tmainCRTStartup()函数的栈顶和栈底的值。
如图:
这里写图片描述
此时对测试程序进行单步调试并检测,ebp压栈,esp-4.
这里写图片描述
继续单步调试(进行2次).
这里写图片描述
可以发现,此时应为当前函数开辟了一段对栈空间。
继续调试,再将ebx、esi、edi寄存器三个寄存器进行压栈完成后,开始对所开辟的堆栈空间进行初始化操作,此时可以调用内存窗口进行查看。
这里写图片描述
可以发现main函数的开辟的临时空间会被全部初始化为CCCCCCCC,我们在VS或者VC下调试程序时候,常常会看到一些没有初始化的变量区值是“烫”,之所以会出现这个奇怪的字,正是因为这里的初始化临时变量的区域为CCCC CCCC造成的。0xCCCC(两个连续排列的0xCC的汉字编码就是烫),所以0xCCCC被当当作文本时候就是烫了。有时候编译器还会使用0xCDCDCDCD作为未初始化的标记,此时我们看到的将不是“烫”,而是“屯”。

继续调试:如下图
这里写图片描述
至此main函数的函数堆栈区域的工作已经完成。跳转到被调函数代码块。
下面是被调函数的汇编代码块

5:int Add(int a,int b)
6:{
    push    ebp
    mov     ebp,esp
    sub     esp,0CCh
    push    ebx
    push    esi
    push    edi
    lea     edi,[ebp-0CCh]
    mov     ecx,33h
    mov     eax,0CCCCCCCCh
    rep stos dword ptr es:[edi]
7: int rete=0;
    mov     dword ptr [rete],0
8:rete = a + b;
    mov     eax,dword ptr [a]
    add     eax,dword ptr [b]
    mov     dword ptr [rete],eax
9:return rete;
    mov     eax,dword ptr [rete]
10: }
    pop    edi
    pop    esi
    pop    ebx
    mov    esp,ebp
    pop    ebp
    ret     

可以看出,函数调用堆栈的开辟方式基本一样。
第一步:保存上一次的ebp(通过压栈操作)保存上个函数的信息,方便出栈时找到。
第二部:将esp的值赋值给ebp,接着给esp一个偏移量,开辟一块函数的临时空间。
第三部:压栈保存三个辅助的寄存器,ebx,esi,edi
第四部:加入调试信息(初始化开辟的临时空间内存)。
第五步:初始化变量
我们需要注意的的是下面的操作:

8:rete = a + b;
    mov     eax,dword ptr [a]
    add     eax,dword ptr [b]
    mov     dword ptr [rete],eax
9:return rete;
    mov     eax,dword ptr [rete]

变量a的位置在哪呢?
变量b呢?
将变量a内存里面的值赋值给eax寄存器,将变量b内存里面的值与a值相加,并将结果存储在寄存器eax中。将eax寄存器的值赋值给rete变量内存中。return rete,可以看到返回时,最终的结果存储在eax寄存器中。
这里写图片描述
接着开始出栈操作

    pop    edi
    pop    esi
    pop    ebx
    mov    esp,ebp
    pop    ebp
    ret     

由于栈的先进后出的特点,所以可以看到先出的是edi寄存器,其它次之。
需要注意的是:当前函数的临时空间是一次全部释放的。
mov esp,ebp此处通过赋值操作直接来释放掉当前函数的临时空间。
通过pop ebp,弹出ebp出栈,返回ebp的值(此时ebp指向上一个函数的栈底,这也是在一开始为什么要把上一个函数ebp压栈的原因)
ret指令返回到调用函数。
这里写图片描述
最后把返回值从eax寄存器拿出给main()函数的变量result.
至此,整个函数调用过程全部完成。

综上所属:我们可以总结几点
1,栈有先进后出的属性。
2,函数调用实参传递时,由调用函数开辟堆栈保存实参,也是由调用函数释放。(和调用惯例有关)
4,函数调用是参数压栈顺序是从右向左。(和调用惯例有关)
3,函数堆栈释放是一次性释放的。
4,return 之后结果是存储在寄存器中临时变量,所以函数调用结束后可以返回值。
5,call指令ret指令可以实现程序的模块化编程。call从调用函数进入到被调函数,ret指令是从被调函数返回到调用函数。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值