凉山州城乡和住房建设厅网站,做图片类型的网站要怎么做,wordpress 注册会员默认权限,工具刷网站排刷排名软件欢迎来到 Claffic 的博客 #x1f49e;#x1f49e;#x1f49e; 前言#xff1a; 用C语言写代码#xff0c;如果一个工程相对复杂时#xff0c;我们往往会采取封装函数的方式。在主函数中调用函数 这一看似简单的过程#xff0c;实际上有很多不宜观察的细节#xff0… 欢迎来到 Claffic 的博客 前言 用C语言写代码如果一个工程相对复杂时我们往往会采取封装函数的方式。在主函数中调用函数 这一看似简单的过程实际上有很多不宜观察的细节这篇博客我将带大家深入探究函数调用的每个细节。 注 内容偏向底层原理可能会比较复杂但我相信看完后你会对函数调用有一个更加深刻的认识。 目录
Part1: 相关问题及概念铺垫
1.几个相关问题
2.寄存器
3.函数栈帧
4.函数调用栈
5.相关汇编指令
Part2: 函数栈帧的创建销毁具体过程
1.前期准备
2. main 函数预开辟栈帧
3.实参的创建和初始化
4.Add函数的调用
5.栈帧的销毁
❤️Part3: 问题答案揭晓 Part1: 相关问题及概念铺垫
1.几个相关问题 • 局部变量是怎么创建的 • 为何局部变量出现屯屯烫烫等随机值 • 函数是怎么传参的传参的顺序 • 实参和形参有何关系 • 函数调用的过程 • 函数调用结束怎么返回 如果没有进行函数栈帧的学习我相信你也会像我当初一样懵逼
好在接下来我会带大家逐步分析每一个过程了解完整个过程后就会豁然开朗~
2.寄存器 寄存器是 CPU 内部用来存放数据的一些小型存储区域用来暂时存放参与运算的数据和运算结果。 常见的寄存器有 eax: 累加(Accumulator)寄存器 , 常用于乘、除法和函数返回值 ebx: 基址(Base)寄存器 , 常做内存数据的指针, 或者说常以它为基址来访问内存 ecx: 计数器(Counter)寄存器 , 常做字符串和循环操作中的计数器 edx: 数据(Data)寄存器 , 常用于乘、除法和 I/O 指针 sbp: 基址指针(Base Point)寄存器 , 只做堆栈指针, 可以访问堆栈内任意地址, 经常用于中转 esp 中的数据 esp: 堆栈指针(Stack Point)寄存器 , 只做堆栈的栈顶指针; 不能用于算术运算与数据传送 有关函数栈帧的是 ebp , esp 这两个寄存器其中存放的是地址
这两个寄存器是用来 维护函数栈帧 的。
3.函数栈帧 C语言中每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。 每一个函数调用都要在 栈区 开辟一段空间。
例如我写下这一段代码
#includestdio.h
//这里把代码拆的很细更加易于看清细节。
int Add(int x, int y)
{int 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;
} main 函数中调用了 Add 函数。 如图所示在栈区为 main 函数开辟了一段空间并且由 ebp 和 esp 这两个寄存器维护。
4.函数调用栈 函数调用栈是一种容器具有后进先出的特性。在函数调用过程中我们利用了栈的特性当调用一个新的函数时进行压栈Push这个函数执行完进行出栈Pop。 简单来说当有函数被调用时该函数就被添加到栈中在执行完所有任务后该栈帧就会被删除。
这时就要问了main 函数也是函数难道还有其他函数调用它吗
是的main 函数也是其他函数调用的不过这在 Visual Studio 2013 中有体现。
下面我以 VS2013 演示 调试 -- 窗口 -- 调用堆栈 此时可以看到 main 函数被调用了 按 F10 继续调试直到程序结束 此时看到了两个陌生的函数
__tmainCRTStartup 和 mainCRTStartup 通过对 crtexe.c 文件的观察我们可以得出下列结论 对应栈帧的开辟 5.相关汇编指令
我们是在反汇编的模式下观察函数栈帧的动作的因此需要一些汇编指令 push数据压入栈pop数据弹出栈 mov数据转移 add加法命令 sub减法命令call函数调用jump转到目标函数进行调用ret恢复返回地址 进行了相关知识的铺垫
那么接下来就是对具体动作的探究了
Part2: 函数栈帧的创建销毁具体过程
1.前期准备
F10 调试 -- 鼠标右键 -- 转到汇编
在反汇编下可以清楚地观察函数栈帧的动作 2. main 函数预开辟栈帧
由于 main 函数是由其他函数调用的所以在调用 main 函数之前就已经开辟好了相关函数的栈帧 00C21410 push ebp //将ebp压入
00C21411 mov ebp,esp //移动esp让其指向压入的ebp;移动ebp让其也指向压入的ebp
00C21413 sub esp, 0E4h //esp减去0E4h指向位置更低的空间相当于为main函数预开辟空间执行完三步后的图示
//依次将ebx,esi,edi压入栈帧
00C21419 push ebx
00C2141A push esi
00C2141B push edi//从edi开始将接下来39h个双字节都改为 OCCCCCCCCh(eax中的内容)
00C2141C lea edi, [ebpFFFFFF1Ch]
00C21422 mov ecx, 39h
00C21427 mov eax, OCCCCCCCCh
00C2142C rep stos dword ptr es:[edi]在 main 函数预开辟之后接下来就要执行有效的代码了
3.实参的创建和初始化
我们继续
int a 10;
//将0A(十进制下是 10)放在 ebp-8 的位置上
00C2142E C7 45 F8 0A 00 00 00 mov dword ptr [ebp-8], 0Ah
int b 20;
//将14(十进制下是 20)放在 ebp-14 的位置上
00C21435 C7 45 EC 14 00 00 00 mov dword ptr [ebp-14h], 14h
int c 0;
//将0(十进制下是 0)放在 qbe-20 的位置上
00C2143C C7 45 E0 00 00 00 00 mov dword ptr [qbe 20], 0执行实参的创建和初始化
4.Add函数的调用
C Add(a, b);
//创建形参并传值
00C21443 8B 45 EC mov eax, dword ptr [ebp-14h]
00C21446 50 push eax
00C21447 8B 4D F8 mov ecx, dword ptr [ebp-8]
00C2144A 51 push ecx
//调用函数记录call下一次指令的地址方便返回
00C2144B E8 91 FC FF FF call 00C210E1
00C21450 83 C4 08 add esp,8
00C21453 89 45 E0 mov dword ptr [ebp- 20h], eax此时才真正进入Add 欸是不是与之前 main 函数的调用有些相似
对的还是先压几个寄存器再填充CCC... 接下来的就是把事先传过来的形参进行运算 调用了数值之后将要返回的结果放入Add函数的栈帧中。 5.栈帧的销毁
//将 edi,esi,ebx 弹出
00C213F1 5F pop edi
00C213F2 5E pop esi
00C213F3 5B pop ebx
//移动 esp,ebp,找到高地址的寄存器
00C213F4 8B E5 mov esp,ebp
00C213F6 5D pop ebp
//返回值
00C213F7 C3 ret最终就把Add函数的栈帧销毁了。
Part3: 问题答案揭晓
回到开头的几个问题在这里做一下回答 • 局部变量是怎么创建的 先创建函数的栈帧在函数栈帧里为局部变量分配空间。 • 为何局部变量出现屯屯烫烫等随机值 在创建函数栈帧时会事先填充CCC...打印出来就是 屯屯烫烫等随机值了所以要养成局部变量初始化的习惯。 • 函数是怎么传参的 在调用函数之前就把参数压栈了当函数中使用参数时再通过指针偏移量找到事先压好的参数 • 实参和形参有何关系 形参是实参的临时拷贝两者的空间独立形参的改变不会改变实参。 • 函数调用的过程 压栈创建空间... • 函数调用结束怎么返回 call 事先记录了下一条指令的地址可以找到此位置再通过寄存器带回。 总结
带大家探究了调用函数时的细节重点是函数栈帧的创建和销毁。
码文不易
如果你觉得这篇文章还不错并且对你有帮助不妨支持一波哦