深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-1
https://developer.aliyun.com/article/1456960
3.main函数中的核心代码
接下来执行以下三句代码:
以a为例子,观察下图:
可得出以下图解:
然后接下来执行以下指令:
首先来看前两条指令:
①
mov 是一个指令,用于将数据从一个位置复制到另一个位置。 eax 是一个32位的寄存器,用于存储通用数据。 dword ptr 是一个修饰符,用于指示后面的操作数应该被视为32位的双字(即4个字节)。 [ebp-14h] 是一个内存引用,它指向位于基址指针 ebp 减去 14h(20个字节)的位置。基址指针 ebp 是一个用于存储局部变量和函数参数的寄存器。 综上所述,这行代码的作用是将位于 ebp-14h 地址处的32位数据加载到 eax 寄存器中
②
push 是一个指令,用于将数据压入堆栈中。 eax 是之前加载了数据的寄存器。
综上所述,这行代码的作用是将 eax 寄存器中的值(20)压入堆栈中
所以,后两条指令同理③④
将位于 ebp-8 地址处的32位数据加载到 ecx 寄存器中,将 ecx 寄存器中的值(10)压入堆栈中
图解:
🧨call指令🚩
函数调用过程
call 是一个指令,用于调用一个函数或子程序。它的作用是将当前指令的下一条指令的地址(返回地址)压入堆栈,并跳转到指定的函数或子程序的地址执行。
按f11,通过call指令就会进入Add函数里面去了(并未真正进入,还要再按一次f11)
call 指令是要执行函数调用逻辑的,在执行call指令之前先会把call指令的下一条指令的地址进行压栈操作,这个操作是为了解决当函数调用结束后要回到call指令的下一条指令的地方,继续往后执行
因此,call 的作用是将当前指令的下一条指令的地址压入堆栈(00C21450),并跳转到地址为 00C210E1 的函数或子程序的入口点执行
4.Add函数栈帧的创建
再按下f11,这时候才是真正来到add函数内,前面那一堆汇编代码跟main函数栈帧创建逻辑是一样的。
反汇编代码:
前提说明
图解:
5.Add函数中的核心代码🎯
反汇编代码:
①:将值0(初始化z)存储到位于内存中的地址 ebp-8 处的双字(32位)数据中。
②将位于内存中地址 ebp+8 处的双字(32位)数据(当前位置的值为10)加载到寄存器 eax 中。
③将位于内存中地址 ebp+0Ch 处的双字(32位)数据(当前位置的值为20)与寄存器 eax 中的值相加,并将结果存储(两数相加的结果为30)回 eax 寄存器中。
④位于当前堆栈帧中相对于基址寄存器 ebp 偏移 8 字节的内存位置的值(当前值为30)复制到寄存器 eax 中
图解:
6.Add函数栈帧的销毁
代码:
这句代码的意思是:
将位于 ebp-8 地址处的32位数据(值为30)加载到寄存器 eax 中,因为函数出去之后,值就销毁了,但是如果放在寄存器eax内就安全了,相当于用了一个全局的寄存器把返回值保存起来,回到主函数main再用。
然后pop三次,把三个寄存器的地址分别弹出:
接着 mov esp,ebp,就是把ebp当前地址赋值给esp:
接着pop ebp,此时ebp回到main函数函数栈帧的栈底:
说明此时Add函数已经销毁了。
此时最重要的一条指令来了:
当pop ebp之后,只是让我们找到了esp和ebp的栈帧空间,但是当我回到main函数的时候,还应该从call指令的下一调指令的地址开始执行,所以此时恰好栈顶上就放着这个地址
这个ret指令return返回的时候,这个指令其实就是从栈顶弹出了call指令下一条指令的地址,然后跳那去了,接着F10走一下,回来main函数内:
存这个地址(00C21450)就是当函数调用完之后还能回来,从call指令的下一条指令的地址开始执行。
所以图解是这样的:
关于形参变量空间的释放:
返回值是怎么带回来:先把值委托到eax寄存器内,接着回到main函数内部赋值
经过esp+8之后,关于x和y两个形参空间的变量就已经销毁,还给操作系统了。
关于main函数的销毁跟上述Add函数的销毁逻辑相似,也不累赘地列举了。
总结:
1.局部变量是如何创建的?
首先为main函数分配栈帧空间,然后在栈帧空间内初始化一部分空间之后,给局部变量在该栈帧空间内分配一点空间
2.为什么局部变量不初始化内容是随机的?
因为随机值是我们放进去的,如果局部变量给它们初始化,那就是把随机值覆盖了。
3.函数调用时参数时如何传递的?传参的顺序是怎样的?
当我要调用那个函数的的时候,就已经push,push,把这两个参数从右向左开始压栈压进去,当我们进入形参函数Add的时候通过指针的偏移量找回来找到了形参
4.形参和实参的关系是什么呢?
形参确实是我在压栈的时候开辟的空间,形参和实参只是值是相同的,空间是独立的,所以形参是实参的一份临时拷贝,改变形参不会影响实参
5.函数调用结束后是如何返回的?
我们在调用之前就已经把call指令的下一条指令的地址压栈压进去了,当函数调用完要返回的时候,弹出ebp就能找到原始上一个函数调用的ebp,然后指针往下走的时候就能中找到esp的地址,接着跳转到call指令下一条指令的地址,返回值是通过寄存器的方式带回来的