网站关于 模板,网页设计师职责,手机系统优化,小智logo在线制作目录 1.什么是函数栈帧2.理解函数栈帧能解决什么问题3.函数栈帧的创建和销毁的过程解析3.1 什么是栈3.2 认识相关寄存器和汇编指令3.3 解析函数栈帧的创建和销毁过程3.3.1 准备环境3.3.2 函数的调用堆栈3.3.3 转到反汇编3.3.4 函数栈帧的创建和销毁 1.什么是函数栈帧
在写C语言… 目录 1.什么是函数栈帧2.理解函数栈帧能解决什么问题3.函数栈帧的创建和销毁的过程解析3.1 什么是栈3.2 认识相关寄存器和汇编指令3.3 解析函数栈帧的创建和销毁过程3.3.1 准备环境3.3.2 函数的调用堆栈3.3.3 转到反汇编3.3.4 函数栈帧的创建和销毁 1.什么是函数栈帧
在写C语言代码的时候我们经常会把一个独立的功能抽象成函数C程序是以函数为基本单位的那么函数又是如何调用的呢函数的参数是怎样传递的呢这些答案都可以在函数栈帧中寻找
函数栈帧stack frame函数调用过程中在程序的调用栈call stack所开辟的空间这些空间是用来存放
函数参数和函数返回值临时变量包括函数的非静态的局部变量以及编译器自动产生的其他临时变量保存上下文信息包括用来维护函数调用前后的寄存器
2.理解函数栈帧能解决什么问题
只要理解好函数栈帧就可以对一下问题有额外的理解
局部变量是如何创建的为什么局部变量不初始化时为随机值函数调用时形参的传递的顺序是怎样的函数的形参和实参的联系是怎样的函数的返回值是如何带回来的
3.函数栈帧的创建和销毁的过程解析
3.1 什么是栈
栈stack是现代计算机程序中最为重要的概念之一几乎每一个程序都要用到栈没有栈就没有函数没有局部变量更没有更正语言的桥接
在经典的计算机科学中栈被定义为一种特殊的容器用户可以将数据压入栈中该操作被称为压栈push也可以将已经压入栈中的数据弹出出栈pop但是栈这个容器遵守一条规则先进后出
在计算机系统中栈则是一个具有以上属性的动态内存区域程序可以将数据压入栈中也可以将栈弹出在经典的操作系统中栈总是向下增长由高地址到低地址的在我们常见的i386或者x86-64下栈顶由esp的寄存器定位
3.2 认识相关寄存器和汇编指令
1.相关寄存器
eax通用寄存器保留临时数据常用于返回值ebx同样寄存器保留临时数据ebp栈底寄存器esp栈顶寄存器与ebp共同维护当前的函数栈帧eip指令寄存器保存当前指令的下一条指令的地址
2.相关汇编命令
mov数据转移指令将后面的数据赋值给前面的数据push数据入栈同时esp寄存器也要发生改变pop数据弹出指定位置同时esp也要发生改变sub减法命令add加法命令call函数调用1.压入返回地址 2.转入目标函数jump通过修改eip转入目标函数进行调用ret恢复返回地址压入eip类似pop eip命令
3.3 解析函数栈帧的创建和销毁过程
3.3.1 准备环境
为了更好地观察函数栈帧的整个过程需要先关闭一些选项以免受到干扰
3.3.2 函数的调用堆栈
这里我们写一段简单的代码并将代码一条一条拆解处理足够好观察内部的细节
注意函数栈帧的创建和销毁过程在不同的编译器的实现方法大同小异但大体的逻辑层次是不会差很多的本次演示用的是VS2019环境
演示代码
#includestdio.hint Add(int x, int y)
{int z 0;z x y;return z;
}int main()
{int a 10;int b 20;int c 0;c Add(a, b);printf(%d\n, c);return 0;
}在VS2019环境下按F10进入调试打开窗口的调用堆栈 调用堆栈是用来反馈函数调用逻辑的
然后继续按F10按完整个主函数进入界面
我们会发现main函数也是被调用的这里可以清晰地观察到invoke_main函数调用了main函数至于是什么函数调用了invoke_main就不再考虑了
3.3.3 转到反汇编
按F10调试到main函数的第一行右击鼠标转到反汇编 注意这里调试出来的地址是由系统自动分配的所以每一次进去调试出来的地址都是不同的 int main()
{
//main函数的函数栈帧的创建
004C1820 push ebp
004C1821 mov ebp,esp
004C1823 sub esp,0E4h
004C1829 push ebx
004C182A push esi
004C182B push edi
004C182C lea edi,[ebp-24h]
004C182F mov ecx,9
004C1834 mov eax,0CCCCCCCCh
004C1839 rep stos dword ptr es:[edi] //main函数中的核心代码int a 10;
004C183B mov dword ptr [ebp-8],0Ah int b 20;
004C1842 mov dword ptr [ebp-14h],14h int c 0;
004C1849 mov dword ptr [ebp-20h],0 c Add(a, b);
004C1850 mov eax,dword ptr [ebp-14h]
004C1853 push eax
004C1854 mov ecx,dword ptr [ebp-8]
004C1857 push ecx//执行call指令会跳转到Add函数内部
004C1858 call 004C10B4
004C185D add esp,8
004C1860 mov dword ptr [ebp-20h],eax printf(%d\n, c);
004C1863 mov eax,dword ptr [ebp-20h]
004C1866 push eax
004C1867 push 4C7B30h
004C186C call 004C10D2
004C1871 add esp,8 return 0;
004C1874 xor eax,eax }调试至call指令按住F11
//Add函数的函数栈帧
int Add(int x, int y)
{
004C1760 push ebp
004C1761 mov ebp,esp
004C1763 sub esp,0CCh
004C1769 push ebx
004C176A push esi
004C176B push edi int z 0;
004C176C mov dword ptr [ebp-8],0 z x y;
004C1773 mov eax,dword ptr [ebp8]
004C1776 add eax,dword ptr [ebp0Ch]
004C1779 mov dword ptr [ebp-8],eax return z;
004C177C mov eax,dword ptr [ebp-8]
}
004C177F pop edi
004C1780 pop esi
004C1781 pop ebx
004C1782 mov esp,ebp
004C1784 pop ebp
004C1785 ret
3.3.4 函数栈帧的创建和销毁
这里我们将拆解每一行汇编代码
//main函数栈帧的创建,在创建之前esp和ebp维护的是invoke_main的函数栈帧
004C1820 push ebp //将ebp寄存器的值进行压栈此时存放的是invoke_main的函数栈帧的ebpesp-4
004C1821 mov ebp,esp //将esp中的值赋给ebp
004C1823 sub esp,0E4h //将esp减去0eE4十六进制的表示此时的esp已经指向了一个新的区域用来维护main的函数栈帧
004C1829 push ebx //把ebx的值进行压栈esp-4
004C182A push esi //把esi的值进行压栈esp-4
004C182B push edi //把edi的值进行压栈esp-4
//上面3条指令保存了3个寄存器的值在栈区这3个寄存器的在函数随后执行中可能会被修改所以先保存寄
//存器原来的值以便在退出函数时恢复。004C182C lea edi,[ebp-24h] //leaload effective address加载有效地址将ebp-24h的地址放到edi中
004C182F mov ecx,9 //将9赋给ecx
004C1834 mov eax,0CCCCCCCCh //将0CCCCCCCCh赋给eax
004C1839 rep stos dword ptr es:[edi]
//从edi开始将以ecx的存储数值为个数的4个字节的数据全部改为eax存储的值
//dworddouble word双字一个字为2个字节双字就是4个字节该段汇编的内存
解释烫烫烫的产生 之所以得到了这些奇怪的汉字是因为这里使用了未初始化的字符数组就导致buf中存储的就是上面的0CCCCCCCCh的值而0xCCCC的汉字编码就是“烫”
//main函数中的核心代码int a 10;
004C183B mov dword ptr [ebp-8],0Ah //将0Ah10赋给ebp-8的地址处,对变量a初始化int b 20;
004C1842 mov dword ptr [ebp-14h],14h //将14h20赋给ebp-14h的地址处对变量b初始化int c 0;
004C1849 mov dword ptr [ebp-20h],0 //将0赋给ebp-20h的地址处对变量c初始化//调用Add函数c Add(a, b);
004C1850 mov eax,dword ptr [ebp-14h] //将ebp-14h地址处的值20存放在eax中这里其实就是传递参数b
004C1853 push eax //把eax的值进行压栈
004C1854 mov ecx,dword ptr [ebp-8] //将ebp-8地址处的值10存放在ecx中这里是传递参数a
004C1857 push ecx //把ecx的值进行压栈该段汇编的内存 //执行call指令会跳转到Add函数内部在跳转之前会进行压栈操作
004C1858 call 004C10B4 //把call指令的下一条的地址进行压栈esp-4,回调函数//Add函数的函数栈帧的创建
004C1760 push ebp //将main函数的ebp的值压栈进行保存esp-4
004C1761 mov ebp,esp //将esp的值赋给ebp
004C1763 sub esp,0CCh //将esp减去0CChesp开始维护新函数Add的函数栈帧
004C1769 push ebx //把ebx的值进行压栈esp-4
004C176A push esi //将esi的值进行压栈esp-4
004C176B push edi //将edi的值进行压栈esp-4
//Add函数中的核心代码int z 0;
004C176C mov dword ptr [ebp-8],0 //将0赋给ebp-8的地址处 z x y;
004C1773 mov eax,dword ptr [ebp8] //将ebp8地址处的值赋给eax
004C1776 add eax,dword ptr [ebp0Ch] //把ebp0Ch地址处的值加到eax中
004C1779 mov dword ptr [ebp-8],eax //将eax中的值赋到ebp-8的地址处return z;
004C177C mov eax,dword ptr [ebp-8] //将ebp-8地址处的值赋给eax
}
该段汇编的内存 可以看出形参和实参的关系形参是实参的一份临时拷贝对形参的修改并不会改变实参
004C177F pop edi //把edi的值进行出栈esp4
004C1780 pop esi //把esi的值进行出栈esp4
004C1781 pop ebx //把ebx的值进行出栈esp4
004C1782 mov esp,ebp //将ebp的值赋给esp
004C1784 pop ebp //把ebp的值进行出栈ebp此时又回到main函数的ebp开始维护main函数的函数栈帧esp4
004C1785 ret //首先弹出栈顶的值此时esp4并回到call指令的下一条地址处继续执行代码
//Add函数的函数栈帧销毁回到call指令的下一条指令
004C185D add esp,8 //esp8
004C1860 mov dword ptr [ebp-20h],eax //将eax的值(存储的就是Add函数的返回值)赋到ebp-20h的地址处printf(%d\n, c);
004C1863 mov eax,dword ptr [ebp-20h] //将ebp-20h地址处的值赋给eax
004C1866 push eax //将eax的值进行压栈
004C1867 push 4C7B30h //将4C7B30h进行压栈
004C186C call 004C10D2 //继续回调函数
004C1871 add esp,8 //esp8return 0;
004C1874 xor eax,eax }该段汇编的内存 小结对于函数栈帧的创建和销毁过程可以在VS上独自进行反汇编代码解析 内存和监视的观察理解该过程更有助于对代码底层的东西了解的更深此外这里对于main函数的函数栈帧的销毁不再解析相信了解过Add函数的整个过程后便能明白上述提出的问题也可以直接回答出