栈帧——函数的调用过程

来源:互联网 发布:嘉靖和万历知乎 编辑:程序博客网 时间:2024/06/06 00:25

结构化程序的一个最基本的单元就是“函数”或者叫“过程”。在汇编这一层自然也相应的有支持这些概念的指令操作,如栈操作和栈帧的概念。

我们知道,一个由C/C++编译的程序占用的内存分为以下几个部分:

1、栈区(stack)— 由编译器自动分配释放 ,存放为运行函数而分配的局部变量、函数参数、返回数据、返回地址等  。                                                                                

2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表 。                                                                           

3、全局区(静态区)(static)—存放全局变量、静态数据、常量。程序结束后由系统释放。

4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放。

5、程序代码区—存放函数体(类成员函数和全局函数)的二进制代码。

其中比较特殊的是堆区和栈区,二者中间有一块很大的内存,堆区申请内存是往高地址处向上生长,而栈地址则是往下增长的。

所以我们今天要研究的就是栈这块的空间。

我们知道,每一次函数的调用的过程,电脑都要为函数开辟栈空间,用于本次函数中临时变量的保存和现场保护。这块栈的空间就被我们称之为函数栈帧。

            上面说过 ,栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。

而栈帧的维护,我们就必须了解以下几个寄存器:

                              ebp(基址寄存器)———指向当前的栈帧的底部(高地址)

                              esp(栈顶寄存器)————指向当前的栈帧的顶部(低地址)

                              eip(指令指针寄存器)——存储着cpu要读取指令的地址,没有它,cpu就无法执行

                              eax,ebx,ecx,edx(数据寄存器)————主要用来保存操作数和运算结果等信息,从而节省读取操作数所需占用总线和访问存储器的时间

                              esi,edi(变址寄存器)———存放存储单元在段内的偏移量


结合此图可以更好地理解栈帧结构

下面,举个栗子:

#define _CRT_SECURE_NO_WARNINGS 1#include #include int mul(int x, int y){int z = 0;z = x*y;return z;}int main(){int a = 4;int b = 5;int ret = mul(a, b);printf("ret=%d", ret);system("pause");return 0;}
反汇编如下:
--- c:\users\lcl\documents\visual studio 2013\projects\栈帧\栈帧\mul.c -------------int main(){00FB13D0  push        ebp                        //压栈00FB13D1  mov         ebp,esp                    //把栈底指针挪到新的位置00FB13D3  sub         esp,0E4                    //扩展新的栈帧,是栈底和栈顶分离,即给esp减去一个16进制数0E4h,产生新的esp00FB13D9  push                                   //依次入栈00FB13DA  push        esi  00FB13DB  push        edi  00FB13DC  lea         edi,[ebp-0E4h]             //lea指令:将有效地址传到指定的寄存器     这里即把栈的最低地址load到edi寄存器   00FB13E2  mov         ecx,39h                    //4*39H=0E4H00FB13E7  mov         eax,0CCCCCCCCh             //把栈帧预开辟的空间全部初始化为0*CCCCCCCC,未初始化则为常见的“烫烫烫烫”00FB13EC  rep stos    dword ptr es:[edi]   int a = 4;00FB13EE  mov         dword ptr [a],4            // 局部变量a的创建,dword 通过指针赋值,ptr就是指针,相当于把4赋值给a int b = 5;00FB13F5  mov         dword ptr [b],5            //同上 int ret = mul(a, b);00FB13FC  mov         eax,dword ptr [b]          //参数b压栈  注意:先把b的值mov到eax寄存器中00FB13FF  push        eax                               //然后b先行入栈00FB1400  mov         ecx,dword ptr [a]          //同上:a接着入栈00FB1403  push        ecx  00FB1404  call        _mul (0FB1127h)            //CALL:a1.将下一条指令所在的地址00FB1409入栈;a2.并将子程序的起始地址送入PC  (即CPU的下一                                                            条指令就转去执行子程序)                                                   即跳转到_mul: 00FB1127  jmp         mul (0FB1480h) //  jmp指令是预示着即将跳转到mul                                                   函数的起始位置00FB1480h00FB1409  add         esp,8                     //栈顶向上移两个变量,8个字节00FB140C  mov         dword ptr [ret],eax       //eax寄存器在mul函数中保存了z的值,现在赋给ret printf("ret=%d", ret);                                      00FB140F  mov         esi,esp                   //esp指向esi00FB1411  mov         eax,dword ptr [ret]       //ptr[ret]=20赋值给eax00FB1414  push        eax                       //eax入栈00FB1415  push        0FB5858h                  //不知道为啥00FB141A  call          dword ptr ds:[0FB9118h] //同上   然后直接跳转到PRINT函数开头      59E7CE70  push        ebp  printf("ret=%d", ret);00FB1420  add         esp,8                     //同上   栈顶向上移00FB1423  cmp         esi,esp                   //cmp指令 :比较两个操作数的大小,第一个操作数减去第二个操作数 ,不保存结果,只影响标志寄存器(SF,ZF,CF,OF)                                                             EXP:mov   ax  ,8      换行   mov    bx, 3    换行   cmp  ax ,bx                                                           执行后 ax=8,ZF=0 ,PF=1,SF=0,CF=0,OF=0                                                           其逻辑含义:ZF=1则AX=BX;    CF=1则AX<BX;   CF=0则AX>=BX00FB1425  call        __RTC_CheckEsp (0FB1140h)   system("pause");                               //system函数00FB142A  mov         esi,esp  00FB142C  push        0FB5860h  00FB1431  call        dword ptr ds:[0FB9110h]  00FB1437  add         esp,4  00FB143A  cmp         esi,esp  00FB143C  call        __RTC_CheckEsp (0FB1140h)   return 0;                                      //return函数00FB1441  xor         eax,eax  }00FB1443  pop         edi  00FB1444  pop         esi  00FB1445  pop         ebx  00FB1446  add         esp,0E4h  00FB144C  cmp         ebp,esp  00FB144E  call        __RTC_CheckEsp (0FB1140h)  00FB1453  mov         esp,ebp  00FB1455  pop         ebp  00FB1456  ret                                   //ending --- c:\users\lcl\documents\visual studio 2013\projects\栈帧\栈帧\mul.c -------------#define _CRT_SECURE_NO_WARNINGS 1#include <stdio.h >#include <Windows.h>int mul(int x, int y)                           //今天的主题——函数调用栈帧{00FB1480  push        ebp                       //main函数的栈底压入栈中,注意:它的上一条指令是00FB140900FB1481  mov         ebp,esp                   //把调用mul()函数前的栈顶esp作为mul()栈帧的栈底ebp00FB1483  sub         esp,0CCh                  //esp往下移开辟空间,新的栈帧已经形成    00FB1489  push        ebx                       //三者依次入栈00FB148A  push        esi  00FB148B  push        edi  00FB148C  lea         edi,[ebp-0CCh]            //将有效地址传给edi寄存器,即取偏移地址00FB1492  mov         ecx,33h                   //跟前面一样4*33h=它的偏移地址0CCh00FB1497  mov         eax,0CCCCCCCCh            //初始化00FB149C  rep stos    dword ptr es:[edi]        //rep指令:重复上面的指令,ecx的值是重复次数;stos指令:将eax的值拷贝esp中(即edi指向的地址) int z = 0;00FB149E  mov         dword ptr [z],0           //创建变量z z = x*y;00FB14A5  mov         eax,dword ptr [x]         //把指针ptr[x]的值赋给eax00FB14A8  imul        eax,dword ptr [y]         //imul函数:将乘数与被乘数均作为有符号数;即eax*=ptr[y]                                                   mul函数:将乘数与被乘数均作为无符号数00FB14AC  mov         dword ptr [z],eax         //把eax的值赋给ptr[z] return z;00FB14AF  mov         eax,dword ptr [z]         //将结果保存到eax寄存器,通过寄存器带回函数的返回值}00FB14B2  pop         edi                       //三者依次出栈00FB14B3  pop         esi  00FB14B4  pop         ebx  00FB14B5  mov         esp,ebp                   //把ebp的内容给esp,即esp箭头指向ebp,ebp,esp同时在ebp处00FB14B7  pop         ebp                       //看第一条指令 ebp指向的是main函数的栈底,然后ebp出栈  (局部变量已经释放)00FB14B8  ret                                   //将函数的返回值地址返回到eip当中,此时将跳转到上面CALL指令的下一条指令--- 无源文件 -----------------------------------------------------------------------*******************************************************************************/int __cdecl printf (        const char *format,        ...        )/* * stdout 'PRINT', 'F'ormatted                   //输出函数print */{59E7CE70  push        ebp                        //知识盲区。59E7CE71  mov         ebp,esp  59E7CE73  push        0FFFFFFFEh  59E7CE75  push        59F95998h  59E7CE7A  push        59F3FEF0h  59E7CE7F  mov         eax,dword ptr fs:[00000000h]  59E7CE85  push        eax  59E7CE86  add         esp,0FFFFFFE4h  59E7CE89  push        ebx  59E7CE8A  push        esi  59E7CE8B  push        edi  59E7CE8C  mov         eax,dword ptr ds:[59FA8100h]  59E7CE91  xor         dword ptr [ebp-8],eax  59E7CE94  xor         eax,ebp  59E7CE96  push        eax  59E7CE97  lea         eax,[ebp-10h]  59E7CE9A  mov         dword ptr fs:[00000000h],eax      va_list arglist;    int buffing;    int retval = 0;59E7CEA0  mov         dword ptr [retval],0  


以上大概就是函数调用时栈帧的全部过程了,如有不对,望大佬们批评指正。


原创粉丝点击