栈帧与函数调用过程分析

一个c/c++编译的程序占用的内存分为以下几个部分:
  1.栈区(stack):由编译器自动分配和释放,存放函数的参数值,局部变量的值,返回数据,返回地址等。操作方式类似于数据结构中的栈。
  2.堆区(heap):一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。与数据结构中的堆是两码事,分配方式类似于链表。
  3.全局区(静态区)(static):存放全局变量、静态数据、常量。程序结束后由系统释放。
  4.文字常量区:常量字符串放在此,程序结束后由系统释放。
  5.程序代码区:存放函数体(类成员函数和全局函数)的二进制代码。
内存区域分配如图所示:

内存分配
其中堆和栈相对而生。
堆和栈的申请方式:
  栈由系统自动分配,速度较快,在windows下栈是向低地址扩展的数据结构,是一块连续的内存区域,大小是2MB。
  堆需要程序员自己申请,并指明大小,速度比较慢。在C中用malloc申请,C++中用new申请。另外,堆是向高地址扩展的数据结构,是不连续的内存区域,堆的大小受限于计算机的虚拟内存。因此堆空间获取和使用比较灵活,可用空间较大。


接下来由一个实例具体分析调用过程

#include <stdio.h>
#include <windows.h>
int Add(int x, int y)
{
    int z = 0;
    z = x + y;
    return z;
}
int main()
{
    int a = 10;
    int b = 20;
    int ret = 0;
    ret = Add(a, b);
    printf("you should run here!\n ret = %d\n", ret);
    system("pause");
    return 0;
}

在具体分析之前先了解3个重要寄存器:
EBP:基址寄存器,存放指向函数栈帧栈底的地址。
ESP:栈顶寄存器,存放指向函数栈帧栈顶的地址。
EIP/IP/PC:程序计数器,保存的内容指向下一条指令。

main函数汇编语言如下:
int main()
{
00E91410  push        ebp      //将ebp压栈处理(方便函数返回之后的现场恢复)
00E91411  mov         ebp,esp    //使esp的值赋给ebp,产生新的ebp
00E91413  sub         esp,0E4h    //给esp减去一个16进制数字0E4h,产生新的esp
00E91419  push        ebx  
00E9141A  push        esi  
00E9141B  push        edi  
00E9141C  lea         edi,[ebp-0E4h]  
00E91422  mov         ecx,39h  
00E91427  mov         eax,0CCCCCCCCh    //把栈帧预开辟的空间全部初始化为0xCCCCCCCC
00E9142C  rep stos    dword ptr es:[edi]  //从edi开始向下重复赋值ecx次,赋值内容为eax的内容
    int a = 10;               
00E9142E  mov         dword ptr [ebp-4],0Ah    
    int b = 20;
00E91435  mov         dword ptr [ebp-8],14h  
    int ret = 0;
00E9143C  mov         dword ptr [ebp-0ch],0  
    ret = Add(a, b);
00E91443  mov         eax,dword ptr [ebp-8]  
00E91446  push        eax  //参数压栈,先压b
00E91447  mov         ecx,dword ptr [ebp-4]  
00E9144A  push        ecx  
00E9144B  call        _Add (0E910E6h)  
00E91450  add         esp,8        //此地址将被压入栈中
00E91453  mov         dword ptr [ret],eax  
    printf("you should run here!\n ret = %d\n", ret);
00E91456  mov         esi,esp  
00E91458  mov         eax,dword ptr [ret]  
00E9145B  push        eax  
00E9145C  push        0E95858h  
00E91461  call        dword ptr ds:[0E99118h]  
00E91467  add         esp,8  
00E9146A  cmp         esi,esp  
00E9146C  call        __RTC_CheckEsp (0E91140h)  
    system("pause");
00E91471  push        0E9587Ch  
00E91476  call        _system (0E910A5h)  
00E9147B  add         esp,4  
    return 0;
00E9147E  xor         eax,eax  
}
00E91490  mov         esp,ebp  
00E91492  pop         ebp  
00E91493  ret  

call的作用:
1.通过修改IP实现函数的跳转(jmp)
2.call将当前正在执行的指令的下一条指令的地址保存起来。

栈帧

Add函数汇编语言如下:
int Add(int x, int y)
{
00E913D0  push        ebp          //将ebp压栈
00E913D1  mov         ebp,esp       //将esp赋值给ebp
00E913D3  sub         esp,0CCh       //给esp减去一个16进制数字
00E913D9  push        ebx  
00E913DA  push        esi  
00E913DB  push        edi  
00E913DC  lea         edi,[ebp-0CCh]  
00E913E2  mov         ecx,33h  
00E913E7  mov         eax,0CCCCCCCCh  
00E913EC  rep stos    dword ptr es:[edi]  
    int z = 0;
00E913EE  mov         dword ptr [ebp-8],0   //创建变量z
    z = x + y;
00E913F5  mov         eax,dword ptr [ebp+8]    //获取形参a和b相加
00E913F8  add         eax,dword ptr [ebp+0ch]  
00E913FB  mov         dword ptr [ebp-4],eax     //将结果存储到z中
    return z;
00E913FE  mov         eax,dword ptr [ebp-4]     //将结果存储到eax寄存器,通过寄存器带回函数的返回值
}

栈帧

00E91401  pop         edi   //出栈
00E91402  pop         esi   //出栈
00E91403  pop         ebx   //出栈,使esp向下移动
00E91404  mov         esp,ebp  //将ebp赋值给esp
00E91406  pop         ebp  
00E91407  ret    //ret指令会使得出栈一次,并将出栈的内容当作地址,将程序执行跳转到该地址处。

栈帧

00E91450  add         esp,8        //此地址将被压入栈中
00E91453  mov         dword ptr [ebp-0Ch],eax  

栈帧

由此分析,x,y是连续存放的,所以我们可以通过修改x的地址进而修改y的值
如:
#include <stdio.h>
#include <windows.h>
int Add(int x, int y)
{
    printf("before y:%d\n", y);
    int *p = &x;
    p++;
    *p = 30;
    printf("after y:%d\n", y);
    return 0;
}
int main()
{
    int a = 10;
    int b = 20;
    int ret = 0;
    ret = Add(a, b);
    printf("you should run here!\n ret = %d\n", ret);
    system("pause");
    return 0;
}
又如:
#include <stdio.h>
#include <windows.h>
void bug()
{
    printf("I am a bug!\n");
    system("pause");
}
int Add(int x, int y)
{
    printf("Add......done!\n");
    int *p = &x;
    p--;
    *p =(int) bug;
    return 0;
}
int main()
{
    int a = 10;
    int b = 20;
    printf("main.....begin!\n");
    int ret = 0;
    ret = Add(a, b);
    printf("you should run here!\n ret = %d\n", ret);
    system("pause");
    return 0;
}

程序执行后崩溃,bug函数调用后没返回到main函数,引起栈结构不平衡。
由此进行改进,使bug函数找到其返回值地址:

#include <stdio.h>
#include<windows.h>

void *c = NULL;
void bug()
{
    int a = 0;
    int *p = &a;
    p += 2;
    *p = (int)c;
    printf("I am a bug!\n");
    system("pause");
}
int Add(int x, int y)
{
    printf("Add......done!\n");
    int *p = &x;
    p--;
    c = (void*)*p;    //将p中的内容保存
    *p =(int) bug;
    return 0;
}
int main()
{
    int a = 10;
    int b = 20;
    printf("main.....begin!\n");
    int ret = 0;
    ret = Add(a, b);
    printf("you should run here!\n ret = %d\n", ret);
    system("pause");
    return 0;
}

  程序还是会崩溃,因为调用Add函数时使用的是call汇编语句,而调用bug函数是直接修改其IP地址。但两个函数最后都有一个ret结构,将栈顶的返回值弹出值EIP中,所以经历了两次出栈,却只有一次压栈。需要平衡栈帧结构。
改进如下:

#include <stdio.h>
#include<windows.h>

void *c = NULL;
void bug()
{
    int a = 0;
    int *p = &a;
    p += 2;
    *p = (int)c;
    printf("I am a bug!\n");
    system("pause");
}
int Add(int x, int y)
{
    printf("Add......done!\n");
    int *p = &x;
    p--;
    c = (void*)*p;    //将p中的内容保存
    *p =(int) bug;
    return 0;
}
int main()
{
    int a = 10;
    int b = 20;
    printf("main.....begin!\n");
    int ret = 0;
    ret = Add(a, b);
    printf("you should run here!\n ret = %d\n", ret);
    _asm
    {
        sub esp,4          //为了平衡栈帧结构
    }
    system("pause");
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值