函数栈帧的创建和销毁(二)

简介: 函数栈帧的创建和销毁

3.3.4函数栈帧的创建


通过上面的汇编代码,接下来一行一行地拆解汇编代码


009118B0  push        ebp  
//把ebp寄存器中的值进行压栈
009118B1  mov         ebp,esp  
//move指令会把esp的值存放到ebp中,此时就产生了mian函数的ebp
009118B3  sub         esp,0E4h  
//sub的作用是使esp中的地址减去一个十六进制的数字0xE4,产生新的esp
//此时esp是main函数栈帧的esp,与上面的ebp之间维护了一块栈空间,
//这块空间就是为main函数开辟的栈空间,这块空间将存储main函数中的
//局部变量,以及调试信息等。
009118B9  push        ebx  
//将寄存器ebx的值压栈,esp-4
009118BA  push        esi  
//将寄存器esi的值压栈,esp-4
009118BB  push        edi  
//将寄存器edi的值压栈,esp-4
//上面三条指令将三个寄存器的值保存在栈区,因为这三个寄存器在函数
//随后的执行过程中可能被修改,如此便避免其中的值被修改,也方便退
//出函数时恢复其中的值
//以下操作便是在初始化main函数的栈帧空间
//1.先把 ebp-0x24h 的地址,放到edi中
//2.把 9 放入ecx
//3.把0CCCCCCCC放在 eax 中
//4.将从 ebp-0x24h 到 ebp 这一段内存的每个字节都初始化为0CCCCCCCC
009118BC  lea         edi,[ebp-24h]  
009118BF  mov         ecx,9  
009118C4  mov         eax,0CCCCCCCCh  
009118C9  rep stos    dword ptr es:[edi]


画图展示如下


ff608bb1b39e67266afe78b8b86cc92a_c48f922c1dbf4dc19275eae756f53c5a.png


平常在写代码没有初始化变量直接打印会出现一连串的烫烫烫,其实打印的就是0xCCCCCCCC


例如


2b6cbedc3945584b7d65c7308ebdccf2_96d71bfd9d5d45899c2fbfb720abc67c.png


接着再分析main函数的核心代码


int a = 3;
00EE18D5  mov         dword ptr [ebp-8],3  
//将3存储到 ebp-8 的地址处,也就是变量a的地址
  int b = 2;
00EE18DC  mov         dword ptr [ebp-14h],2  
//将2存储到 ebp-14h 的地址处,也就是变量b的地址
  int ret = 0;
00EE18E3  mov         dword ptr [ebp-20h],0  
//将0存储到 ebp-20h 的地址处,就是变量 ret 的地址
//以上汇编代码的本质就是变量 a,b,ret 的创建和初始化,就是局部变量
//的创建和初始化
//局部变量是在局部变量所在的函数的栈帧空间中所创建的

画图展示如下


b15801e4759248891d7acffdfd31088d_cbd8544abf964a7183fff1cbab271c86.png


Add函数的传参


//调用Add函数
  ret = Add(a, b);
//调用Add函数时的传参,是把参数压栈到栈帧空间
00EE18EA  mov         eax,dword ptr [ebp-14h]  寄存器中
//传递b,将 ebp-14h 地址处的2存放在 eax 
00EE18ED  push        eax  
//将 eax 的值进行压栈,esp-4
00EE18EE  mov         ecx,dword ptr [ebp-8]  
//传递a,将 ebp-8 地址处的3存放在 ecx 寄存器中
00EE18F1  push        ecx  
//将 ecx 的值进行压栈,esp-4
//跳转调用函数
00EE18F2  call        00EE13B6  
00EE18F7  add         esp,8  
00EE18FA  mov         dword ptr [ebp-20h],eax


1fd2e598d5c05a02b7e2874376f811fd_55199c5b42f147ee827b95339d009475.png


00EE18F2  call        00EE13B6  
00EE18F7  add         esp,8  
00EE18FA  mov         dword ptr [ebp-20h],eax


call指令是要执行函数调用逻辑的,在执行 call指令之前会先把 call指令下一条指令的地址进行压栈操作,此操作是为了解决当函数调用结束后要回到 call指令的下一条指令的地址处,继续执行程序。


2ffe535de5533e38ab8c6940266a23df_c6411f8e12e44dbdbc868bfaca3c2c01.png


当跳转到Add函数时,就要开始观察Add函数的反汇编代码


int Add(int x, int y)
{
00EE1830  push        ebp  
//保存 main函数 栈帧的 ebp ,esp-4
00EE1831  mov         ebp,esp  
//将main函数的 esp 赋值给新的 ebp,ebp就变成 Add 函数的 ebp
00EE1833  sub         esp,0CCh  
//sub的作用是使esp中的地址减去一个十六进制的数字0CCh,产生新的esp
//此时esp是Add函数栈帧的esp,与上面的ebp之间维护了一块栈空间,
//这块空间就是为Add函数开辟的栈空间,这块空间将存储Add函数中的
//局部变量,以及调试信息等
00EE1839  push        ebx  
//将 ebx 的值进行压栈,esp-4
00EE183A  push        esi  
//将 esi 的值进行压栈,esp-4
00EE183B  push        edi  
//将 edi 的值进行压栈,esp-4
00EE183C  lea         edi,[ebp-0Ch]  
00EE183F  mov         ecx,3  
00EE1844  mov         eax,0CCCCCCCCh  
00EE1849  rep stos    dword ptr es:[edi]  
00EE184B  mov         ecx,0EEC008h  
00EE1850  call        00EE131B  
  int z = 0;
00EE1855  mov         dword ptr [ebp-8],0  
//将0存放在 ebp-8 的地址处,其实就是 z 的地址处
  z = x + y;
00EE185C  mov         eax,dword ptr [ebp+8]  
//将 ebp+8 的地址处的数字存储到 eax 中
00EE185F  add         eax,dword ptr [ebp+0Ch]  
//将 ebp+0xC地址处的数字加到 eax 中
00EE1862  mov         dword ptr [ebp-8],eax  
//将 eax 的结果保存到 ebp-8 的地址处,也就是 z 的地址处
  return z;
00EE1865  mov         eax,dword ptr [ebp-8]  
//将 ebp-8 地址处的值存放在 eax 中,本质上就是将 z 的值存储到
//eax 中,通过 eax 带回计算的结果,作为函数的返回值
}
00EE1868  pop         edi  
00EE1869  pop         esi  
00EE186A  pop         ebx  
00EE186B  add         esp,0CCh  
00EE1871  cmp         ebp,esp  
00EE1873  call        00EE1244  
00EE1878  mov         esp,ebp  
00EE187A  pop         ebp  
00EE187B  ret


代码执行到Add函数时,就需要开始创建Add函数的栈帧空间

在Add函数中创建栈帧的方法与在main函数相似。


创建Add函数栈帧的整体思路


1. 将mian函数的 ebp 压栈
 2. 计算新的 ebp 和 esp
 3. 将 ebx,esi,edi 寄存器的值保存
 4. 计算求和,在计算的过程中,通过 ebp 的地址访问函数调用前压栈
 进去的参数,也就是形参访问
 5.将求出的和放在 eax 寄存器中带回


c7f36d991fee9ff834a93bef68450f22_32a81328dffa4f8b8b6ada0091615b2e.png


上图中的a'和b'其实是Add函数的形参x,y。图中就很好地说明函数在传参过程中,以及函数在进行传值调用时,形参就是实参的一份临时拷贝,对形参的修改不会影响实参。


3.3.5函数栈帧的销毁


当函数调用即将结束时,前面创建的函数栈帧也即将开始销毁

接下来,通过反汇编代码来具体了解是怎么销毁的


00EE1868  pop         edi  
//在栈顶弹出一个值,存放到 edi 中,esp+4
00EE1869  pop         esi  
//在栈顶弹出一个值,存放到 esi 中,esp+4
00EE186A  pop         ebx  
//在栈顶弹出一个值,存放到 ebx 中,esp+4
00EE186B  add         esp,0CCh  
00EE1871  cmp         ebp,esp  
//再次将Add函数的 ebp 的值赋值给 esp ,相当于回收了Add函数的
栈帧空间
00EE1873  call        00EE1244  
00EE1878  mov         esp,ebp  
00EE187A  pop         ebp  
//弹出栈顶的值存放到 ebp,栈顶此时的值恰好是main函数的 ebp,esp+4
//此时恢复了main函数的栈帧维护,esp指向mian函数栈帧的栈顶,
ebp指向了main函数栈帧的栈底
00EE187B  ret  
//ret指令的执行,首先是从栈顶弹出一个值,此时栈顶的值就是call指令
下一条指令的地址,此时 esp+4 ,接着就直接跳转到call指令下一条指令
的地址处,举行执行程序。


回到call指令的下一条指令的地址处


00EE18F7  add         esp,8  
00EE18FA  mov         dword ptr [ebp-20h],eax  
  printf("%d\n", ret);
00EE18FD  mov         eax,dword ptr [ebp-20h]  
00EE1900  push        eax  
00EE1901  push        0EE7B30h  
00EE1906  call        00EE10D2  
00EE190B  add         esp,8


调用完Add函数,回到函数时,继续执行后面的代码。


00EE18F7  add         esp,8  
//esp加上8,等价于跳过main函数中压栈的 a'和b' 
00EE18FA  mov         dword ptr [ebp-20h],eax  
//将 eax 中的值,存放到 ebp-0x20h的地址处,也就是存储到main函数
//中的变量 ret 中,eax 中的值就是Add函数中计算的 x 和 y 的和,显而
//易见,本次函数的返回值是由 eax 寄存器带回来的,程序是在函数调用
//返回之后,在 eax 中读取返回值的。

目录
相关文章
|
6月前
|
编译器
函数栈帧的创建和销毁
函数栈帧的创建和销毁
32 0
|
7月前
|
存储 编译器 容器
函数栈帧的创建和销毁讲解
函数栈帧的创建和销毁讲解
42 0
|
7月前
|
编译器 容器
关于函数栈帧的创建和销毁
关于函数栈帧的创建和销毁
|
存储
函数栈帧的创建和销毁(下)
函数栈帧的创建和销毁(下)
54 0
|
7月前
|
容器
函数栈帧的创建和销毁介绍
函数栈帧的创建和销毁介绍
41 0
|
7月前
|
存储 编译器
初识函数栈帧的创建与销毁(笔记)
初识函数栈帧的创建与销毁(笔记)
|
编译器 程序员 C语言
函数栈帧的创建与销毁(超详解)
函数栈帧的创建与销毁(超详解)
107 0
|
存储 缓存 编译器
函数栈帧的创建与销毁
函数栈帧的创建与销毁
41 0
|
存储 C语言 C++
你知道函数栈帧的创建和销毁吗?
你知道函数栈帧的创建和销毁吗?
76 0
|
存储 编译器 C++
深入理解内存 —— 函数栈帧的创建与销毁
深入理解内存 —— 函数栈帧的创建与销毁
125 0