函数栈帧的创建和销毁,带动图详细解析,带你大致分析汇编代码

发布于:2024-06-21 ⋅ 阅读:(117) ⋅ 点赞:(0)

目录

1.什么是函数栈帧

2.理解函数栈帧有什么用?

3.函数栈帧的创建和销毁解析

3.1什么是栈?

3.2 认识相关寄存器和汇编指令

3.3函数栈帧的创建和销毁解析过程

3.4函数的调用

3.5汇编代码

3.5.1函数栈帧的创建

3.5.2main函数部分

3.5.3Add函数部分

3.5.4main函数剩下部分


1.什么是函数栈帧

  1. 在写C语言程序的时候,经常为了实现一个功能来封装一个函数,C语言是以函数为基础的基本单位
    1. 函数是怎么调用的?
    2. 函数是怎么传参的?
    3. 函数的返回值怎么带回?
  2. 上面这些问题都与函数栈帧有关系
  3. 函数栈帧(stack frame),在函数调用时,系统会调用栈(call stack)所开辟空间,这些空间用来存放:
    1. 函数参数和函数返回值
    2. 临时变量(函数静态局部变量和编译器自动产生的其他临时变量)
    3. 保存上下文信息(需要保持不变的寄存器)

2.理解函数栈帧有什么用?

  1. 只要理解了函数栈帧的创建和销毁,以下的问题可以很好的理解了
    1. 局部变量是如何创建的?
    2. 局部变量在不初始化内容的情况下为什么是随机的?
    3. 函数调用时参数如何传递的?
    4. 传参的先后顺序是怎样的?
    5. 函数的返回值如何返回?
  2. 以上问题学习完,下面的都可以得到答案了

3.函数栈帧的创建和销毁解析

3.1什么是栈?

  1. 现在计算机程序都会用到栈,有了栈才有了函数和局部变量,才会有现在的计算机语言
  2. 这个栈是内存中的栈和数据结构中的不一样的,要区分开;栈的入数据(Push),入数据只会在栈顶,栈的出数据(Pop),这只会在堆顶;遵循着后进先出的原则
  3. esp:是指向栈顶的,Pop数据或者Push数据,esp的指向会刷新
  4. 栈是由高到低增长的(高地址处向低地址处增长)

3.2 认识相关寄存器和汇编指令

  1. 相关寄存器
    1. eax:通用寄存器,保留临时数据,常用于返回值
    2. ebx:通用寄存器,保留临时数据
    3. ebp:栈底寄存器
    4. esp:栈顶寄存器
    5. eip:指令寄存器,保存当前指令的下一条指令的地址
  1. 相关汇编命令
    1. mov:数据转移指令
    2. push:数据入栈,同时esp栈顶寄存器也要发生改变
    3. pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
    4. sub:减法命令
    5. add:加法命令
    6. call:函数调用,1. 压入返回地址 2. 转入目标函数
    7. jump:通过修改eip,转入目标函数,进行调用
    8. ret:恢复返回地址,压入eip,类似pop eip命令

3.3函数栈帧的创建和销毁解析过程

  1. 每个编译器下,栈帧的创建是略有差异的
  2. 寄存器:eax、ebx、ecx、edx;重点是ebp和esp
    1. esp:栈顶寄存器
    2. ebp:栈底寄存器
  3. ebp、esp 这2个寄存器中存放的是地址使用这两个地址来维护函数栈帧
    1. 每个函数都有自己ebp和esp来维护函数
  4. 每一个函数调用,都要在栈区上开辟一块空间
  5. 寄存器是用来存储数据的,不管是什么,只用来存储数据

3.4函数的调用

  1. 其实main函数在最开始是被其他函数调用的,有另外一个函数在调用
  2. 图片中调试窗口调用堆栈,是有invoke_main()函数在调用我们的main函数,看第二张图
  3. 如果想打开,调用堆栈这个调试窗口可以这么做:调试 -->窗口 --> 调用堆栈

图二:从调试图中确实如此,的确是invoke_main函数在调用main函数;

图三:调用的invoke_main函数

3.5汇编代码   

  1. 汇编调试所用到的代码
  2. 我是用的环境是vs2022,x86

int main()
{
009C25B0  push        ebp  
009C25B1  mov         ebp,esp  
009C25B3  sub         esp,0E4h  
009C25B9  push        ebx  
009C25BA  push        esi  
009C25BB  push        edi  
009C25BC  lea         edi,[ebp-24h]  
009C25BF  mov         ecx,9  
009C25C4  mov         eax,0CCCCCCCCh  
009C25C9  rep stos    dword ptr es:[edi]  
009C25CB  mov         ecx,9CC008h  
009C25D0  call        009C1320  
int a = 10;
009C25D5  mov         dword ptr [ebp-8],0Ah  
int b = 5;
009C25DC  mov         dword ptr [ebp-14h],5  
int ret = 0;
009C25E3  mov         dword ptr [ebp-20h],0  
ret = Add(a, b);
009C25EA  mov         eax,dword ptr [ebp-14h]  
009C25ED  push        eax  
009C25EE  mov         ecx,dword ptr [ebp-8]  
009C25F1  push        ecx  
009C25F2  call        009C13CA  
009C25F7  add         esp,8  
009C25FA  mov         dword ptr [ebp-20h],eax  
printf("%d\n", ret);
009C25FD  mov         eax,dword ptr [ebp-20h]  
009C2600  push        eax  
009C2601  push        9C7BCCh  
009C2606  call        009C13CF  
009C260B  add         esp,8  
return 0;
009C260E  xor         eax,eax  
}

所用到的代码

#include <stdio.h>
int Add(int x, int y)
{
	int z = 0;
	z = x + y;
	return z;
}
int main()
 {
	int a = 10;
	int b = 5;
	int ret = 0;
	ret = Add(a, b);
	printf("%d\n", ret);
	return 0;
}
  1. invoke_main以上的函数就不考虑了,接下来转到反汇编,从main函数的第一行开始

3.5.1函数栈帧的创建

注意:每次调试的地址名字不一样,但是逻辑是一样的

int main()
{
002F17E0  push        ebp//把ebp寄存器中的值进行压栈,此时的ebp中存放的是
    //invoke_main函数栈帧的ebp,esp-4  
002F17E1  mov         ebp,esp //将esp的地址给ebp,ebp走到esp位置 
002F17E3  sub         esp,0E4h//esp - 0E4h esp向低地址走,为main函数预开辟空间 
002F17E9  push        ebx     //push ebx esi edi 三个寄存器到栈顶 这三个值随时有可能被修改
002F17EA  push        esi  
002F17EB  push        edi  
002F17EC  lea         edi,[ebp-24h]  // 刷新edi寄存器的位置,就是为main开辟的那块空间进行
002F17EF  mov         ecx,9          // 初始化操作
002F17F4  mov         eax,0CCCCCCCCh // 三行代码结合理解,将这块空间edi 到 ebp之前的9个值全部初始化成0CCCCCCCCh
002F17F9  rep stos    dword ptr es:[edi]  
002F17FB  mov         ecx,2FC008h    // 把对应地址的内容放到寄存器中,寄存器只用来存储数据
002F1800  call        002F1320       // 在执行对应地址指向的函数之前,会先存储下一个指令的地址
    //下一个指令的地址就是 002F1805
	int a = 10;
002F1805  mov         dword ptr [ebp-8],0Ah  
}
  1. 画图理解图,当然后面也有动图理解;编译器部分的调试,大家就多试试,结合着理解

  1. 寄存器是存储数据的,是获取地址中的内容保存到寄存器,或者把寄存器的值保存到内存的对应地址中

3.5.2main函数部分

  1. 经过上面操作,main函数的函数栈帧开辟好了
int main()
{
	int a = 10;
002F1805  mov         dword ptr [ebp-8],0Ah   //把10放到ebp - 8的位置上
	int b = 20;
002F180C  mov         dword ptr [ebp-14h],14h //把20放到ebp - 14h的位置上 

	int ret = add(a, b);                      //实参对形参的拷贝
002F1813  mov         eax,dword ptr [ebp-14h] //拷贝ebp - 14h位置的值,放到eax寄存器
002F1816  push        eax  //然后压栈
002F1817  mov         ecx,dword ptr [ebp-8]   //拷贝ebp - 8位置的值,放到ecx
002F181A  push        ecx  
002F181B  call        002F1023  //调用Add函数前,栈顶保存下一条指令的值,就是002F1820
002F1820  add         esp,8     // 调用函数回来后要销毁 形参拷贝的值
002F1823  mov         dword ptr [ebp-20h],eax   // 放到 eax寄存器里的值,给ebp - 20h,ret
	return 0;
002F1826  xor         eax,eax  
}
  1. 这里有个知识的分享,是不是有时候会打印出或报错出现中文 “烫烫烫烫----”这些字,为什么会这样?其实都是有原因的,
  2. 在内存中就是CCCCCC的初始化,翻译成中文就是“烫烫烫烫----”了,如果出现这样的报错,多半是使用了未初始化的空间
  3. 在执行call指令是要按F11逐行调试,最后会到Add函数中,下面也会讲到

这里可以看看调试图确实是和说的一样,注意:ebp-14h,-减的16进制的,14h == 20

在执行Add函数前到底会不会存储下一条指令的地址,看下面图片解析

这里为什么是倒着存储的,可以去看看这篇博客  整形数据与浮点型的数据在内存中存储的形式,以及大小端字节序(笔记版)

3.5.3Add函数部分

  1. 最开始就可以观察到,Add函数前面部分和main函数逻辑一样的函数栈帧的创建
  2. 最重要的是自己去调试!!!
int Add(int x, int y)
{
009C1790  push        ebp  
009C1791  mov         ebp,esp  
009C1793  sub         esp,0CCh  
009C1799  push        ebx  
009C179A  push        esi  
009C179B  push        edi  
009C179C  lea         edi,[ebp-0Ch]  
009C179F  mov         ecx,3  
009C17A4  mov         eax,0CCCCCCCCh  
009C17A9  rep stos    dword ptr es:[edi]  
009C17AB  mov         ecx,9CC008h  
009C17B0  call        009C1320       //前面函数栈帧的创建就略过了
	int z = 0;
009C17B5  mov         dword ptr [ebp-8],0   //创建变量Z,把值放到ebp - 8的位置上
	z = x + y;
009C17BC  mov         eax,dword ptr [ebp+8] //先取到ebp+8,也就是变量a的拷贝,也就是现在的x,值放到eax寄存器
009C17BF  add         eax,dword ptr [ebp+0Ch] //再取到变量b的拷贝,然后相加,结果放到eax
009C17C2  mov         dword ptr [ebp-8],eax   // 把最终的结果放到ebp - 8,创建的Z变量的位置
	return z;
009C17C5  mov         eax,dword ptr [ebp-8]  //出函数就会销毁变量,所以暂时放到eax寄存器中
}
009C17C8  pop         edi  //Pop,三次,因为变量销毁,空间回收
009C17C9  pop         esi  
009C17CA  pop         ebx  
009C17CB  add         esp,0CCh  //回收之前预开辟的空间
009C17D1  cmp         ebp,esp   //比较ebp和esp位置的值
009C17D3  call        009C1244  //这里调用了其他函数,我也不太清楚
009C17D8  mov         esp,ebp   //把ebp的地址给esp,esp走到ebp的位置
009C17DA  pop         ebp       //Pop ebp,也就是压栈的main函数的ebp
009C17DB  ret                   //ret,回到call下一条指令的位置,可以发现这个程序很严谨
  1. 板书解释
  2. 到了这个,再回看到之前提出的问题,现在应该都可以得到解释了
  3. 前面的Add函数栈帧部分大概的说一下

3.5.4main函数剩下部分

  1. 剩下的部分就粗略说明一下了,函数栈帧的销毁上面的很详细
	ret = Add(a, b);
009C25EA  mov         eax,dword ptr [ebp-14h]  
009C25ED  push        eax  
009C25EE  mov         ecx,dword ptr [ebp-8]  
009C25F1  push        ecx  
009C25F2  call        009C13CA  
009C25F7  add         esp,8  
009C25FA  mov         dword ptr [ebp-20h],eax  
	printf("%d\n", ret);
009C25FD  mov         eax,dword ptr [ebp-20h]  
009C2600  push        eax  
009C2601  push        9C7BCCh  
009C2606  call        009C13CF  
009C260B  add         esp,8  
	return 0;
009C260E  xor         eax,eax  
}
009C2610  pop         edi  
009C2611  pop         esi  
009C2612  pop         ebx  
009C2613  add         esp,0E4h  
009C2619  cmp         ebp,esp  
009C261B  call        009C1244  
009C2620  mov         esp,ebp  
009C2622  pop         ebp  
009C2623  ret


网站公告

今日签到

点亮在社区的每一天
去签到