目录
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.什么是函数栈帧
- 在写C语言程序的时候,经常为了实现一个功能来封装一个函数,C语言是以函数为基础的基本单位
- 函数是怎么调用的?
- 函数是怎么传参的?
- 函数的返回值怎么带回?
- 上面这些问题都与函数栈帧有关系
- 函数栈帧(stack frame),在函数调用时,系统会调用栈(call stack)所开辟空间,这些空间用来存放:
- 函数参数和函数返回值
- 临时变量(函数静态局部变量和编译器自动产生的其他临时变量)
- 保存上下文信息(需要保持不变的寄存器)
2.理解函数栈帧有什么用?
- 只要理解了函数栈帧的创建和销毁,以下的问题可以很好的理解了
- 局部变量是如何创建的?
- 局部变量在不初始化内容的情况下为什么是随机的?
- 函数调用时参数如何传递的?
- 传参的先后顺序是怎样的?
- 函数的返回值如何返回?
- 以上问题学习完,下面的都可以得到答案了
3.函数栈帧的创建和销毁解析
3.1什么是栈?
- 现在计算机程序都会用到栈,有了栈才有了函数和局部变量,才会有现在的计算机语言
- 这个栈是内存中的栈和数据结构中的不一样的,要区分开;栈的入数据(Push),入数据只会在栈顶,栈的出数据(Pop),这只会在堆顶;遵循着后进先出的原则
- esp:是指向栈顶的,Pop数据或者Push数据,esp的指向会刷新
- 栈是由高到低增长的(高地址处向低地址处增长)
3.2 认识相关寄存器和汇编指令
- 相关寄存器
- eax:通用寄存器,保留临时数据,常用于返回值
- ebx:通用寄存器,保留临时数据
- ebp:栈底寄存器
- esp:栈顶寄存器
- eip:指令寄存器,保存当前指令的下一条指令的地址
- 相关汇编命令
- mov:数据转移指令
- push:数据入栈,同时esp栈顶寄存器也要发生改变
- pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
- sub:减法命令
- add:加法命令
- call:函数调用,1. 压入返回地址 2. 转入目标函数
- jump:通过修改eip,转入目标函数,进行调用
- ret:恢复返回地址,压入eip,类似pop eip命令
3.3函数栈帧的创建和销毁解析过程
- 每个编译器下,栈帧的创建是略有差异的
- 寄存器:eax、ebx、ecx、edx;重点是ebp和esp
- esp:栈顶寄存器
- ebp:栈底寄存器
- ebp、esp 这2个寄存器中存放的是地址,使用这两个地址来维护函数栈帧
- 每个函数都有自己ebp和esp来维护函数
- 每一个函数调用,都要在栈区上开辟一块空间
- 寄存器是用来存储数据的,不管是什么,只用来存储数据
3.4函数的调用
- 其实main函数在最开始是被其他函数调用的,有另外一个函数在调用
- 图片中调试窗口调用堆栈,是有invoke_main()函数在调用我们的main函数,看第二张图
- 如果想打开,调用堆栈这个调试窗口可以这么做:调试 -->窗口 --> 调用堆栈
图二:从调试图中确实如此,的确是invoke_main函数在调用main函数;
图三:调用的invoke_main函数
3.5汇编代码
- 汇编调试所用到的代码
- 我是用的环境是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;
}
- 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
}
- 画图理解图,当然后面也有动图理解;编译器部分的调试,大家就多试试,结合着理解
- 寄存器是存储数据的,是获取地址中的内容保存到寄存器,或者把寄存器的值保存到内存的对应地址中
3.5.2main函数部分
- 经过上面操作,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
}
- 这里有个知识的分享,是不是有时候会打印出或报错出现中文 “烫烫烫烫----”这些字,为什么会这样?其实都是有原因的,
- 在内存中就是CCCCCC的初始化,翻译成中文就是“烫烫烫烫----”了,如果出现这样的报错,多半是使用了未初始化的空间
- 在执行call指令是要按F11逐行调试,最后会到Add函数中,下面也会讲到
这里可以看看调试图确实是和说的一样,注意:ebp-14h,-减的16进制的,14h == 20
在执行Add函数前到底会不会存储下一条指令的地址,看下面图片解析
这里为什么是倒着存储的,可以去看看这篇博客 整形数据与浮点型的数据在内存中存储的形式,以及大小端字节序(笔记版)
3.5.3Add函数部分
- 最开始就可以观察到,Add函数前面部分和main函数逻辑一样的函数栈帧的创建
- 最重要的是自己去调试!!!
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下一条指令的位置,可以发现这个程序很严谨
- 板书解释
- 到了这个,再回看到之前提出的问题,现在应该都可以得到解释了
- 前面的Add函数栈帧部分大概的说一下
3.5.4main函数剩下部分
- 剩下的部分就粗略说明一下了,函数栈帧的销毁上面的很详细
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