目录
"函数栈帧的创建和销毁"这个知识点使我知道我初学程序语言时打印烫烫烫乱码的原因,另外也让我搞懂了一些代码内部的工作方式,函数栈帧的创建和销毁无疑是一种修炼内功的方式。
这篇内容看上去很复杂,一串连下来,其实很容易掌握,所以需要耐心,干货多多,细节饱满。
函数栈帧大概分为创建、内部实现和销毁,ebp称为栈底指针,esp称为栈顶指针,共同维护一块函数栈帧,esp再次使用时向上移动且移动到栈顶,ebp的再次使用也是需要修改的。
eax是用来存放需要修改的值,edi是用来存放需要修改(一块空间所有的值)的地址,ebx是用来存放从edi地址向下需要修改值的次数,连起来就是将edi地址向下ecx次全部初始化为eax的值,eax,ebx,ecx,edx都属于寄存器。
压栈是在栈顶压,即push,出栈是在栈顶出,即pop,这些寄存器就是压栈来搞一块属于自己的地址。
这里我用VS2019演示实际的函数栈帧的创建和销毁,VS2013更好,版本越高简化的越多,底部具体步骤相比更简单。
调用堆栈可以看函数与函数之间的调用关系,比如main函数是由__mainCRTstartup调用(vs2013可以查看)
函数栈帧的创建
调试时右键代码区域转到反汇编,可以查看汇编代码
为了查看的更细致,右键将显示符号名关掉
从上往下,一步一步来。
push压栈ebp,在__mainCRTstartup栈顶压栈,mov是后者移动到前者,因为esp和ebp都是指针变量,所以将esp的值改为ebp的值,sub是前者减后者,esp-0E4h,逻辑上的一块栈帧,高地址在下,低地址在上,这里esp向上移动,栈顶压栈ebx,esi,edi,随后lea是指load effective address加载有效地址,rep stos的作用是edi加载地址向下ecx次全部初始化为0CCCCCCCCh,这就是打印出烫烫烫乱码的原因,其中dword的word是两个字节,dword是四个字节,其次发现寄存器并不是特定的需要存特定的值,call意思为调用
内存窗口发现函数栈帧的创建已经成功了,一块很大的区域(这里只是展示了一部分)全部初始化为cccccccc,上面的call调用的不必关心。
继续往下走~
函数内部的实现
ebp-8,就是栈底指针与我们要用的4个字节(dword)的指针之间隔了8个字节,相当于栈底指针是一块,向上隔一块,0Ah16进制,A*16^0=10,所以ebp-8的地址就是为a所用的,b和c也就迎刃而解了
其次,压栈存放20的eax,压栈存放10的ecx,F11调用Add函数
压栈ebp,把原来esp的值传给ebp,这里画图演示,esp自动向上移动
return x+y上面的都是函数栈帧的创建,ebp+8就是ecx,20,ebp+0ch(12)就是上图eax,10,这里足以证明形参是实参的一份临时拷贝,不会影响实参的值,不会在函数内部新开辟一块空间,eax最后等于30,寄存器不会销毁,所以这就是为什么返回值不会销毁的原因。
函数调用结束后返回值的返回和栈帧的销毁
vs2013
vs2019
一条指令一条指令走,pop出栈edi、esi、ebx,回过头看Add函数调用时的汇编代码第三行,再看0c0h,一目了然,这个需要思考一下,其实跟着代码一步一步走,就是把Add函数栈帧销毁了而已。
函数栈帧用完后就销毁了,其次就是看返回值eax是怎么执行的
esp+8指到存放eax的地址处
ebp-20就是存放c的地址,把c mov 成eax(30)就ok了。