函数调用的实质-结合栈帧探讨(Stack Frame)
前言
在撰写 函数调用的实质 时,接触到了栈帧的概念。
为了更好的理解 函数调用的实质,故对该概念也进行了相关的整理。
栈帧是什么
众所周知,栈是一种数据结构。其符合FILO(First in last out) 的原则,该原则在计算机世界中十分的常见。因而栈作为一种数据结构被广泛的使用于计算机中。
在计算机中程序中,调用栈( Call Stack ) 为程序正在使用的栈空间,由多个嵌套函数所使用的栈帧组成。
栈帧( Stack Frame) 即为单个函数所分配的栈空间。
对于栈帧,计算机使用两个寄存器对其进行跟踪:sp(stack pointer) & bp(based pointer)
ps:在32位机器中为 esp & ebp,64位为 rsp & rbp
对于程序而言,栈从高地址向低地址延伸,堆从低地址从高地址延伸。堆栈之间有大块空闲存储区。
寄存器bp指向栈帧的底部(高地址),寄存器sp指向栈帧的顶部(低地址)
准备工作
准备一段代码:
|
|
通过 g++ 编译输出 汇编代码
如下【省略无关紧要的代码】,请先不要阅读以下代码。翻过它,跟着文章的分析部分进行理解。
|
|
代码分析
首先,我们对main函数进行剖析
|
|
在前文,我们指出bp,sp寄存器分别为基址指针寄存器与栈指针寄存器。调用main函数的时候,main函数对ebp指针进行压栈操作,保存上一个栈帧的栈顶。并且将esp的值初始化为当前的栈顶值【当前栈顶即为栈底】。
而后,subl语句。对esp寄存器进行了减40操作,为main函数创建了大小为40个字节的栈帧
接下来考虑如下代码
|
|
回归c++代码,调用函数为foo(1,2)。因而,该汇编代码向我们展示了以下几个事实:
- main函数初始参数为0
- 调用函数前,函数参数从右向左被推入栈中。movl $1,(%esp)将参数1推入main栈帧帧底,movl $2, 4(%esp)将参数2推入main栈帧帧底加4个字节。【栈从高往低延伸】
- 局部变量一开始被寄存于寄存器,而后因为溢出,而被存放到了内存【十分不合理的感觉,望高人赐教】
接下来,我们进入第一个函数foo,在汇编代码中其为__Z3fooii
|
|
- 保存main函数的基指针,也就是保存main函数栈帧的栈底 [pushl %ebp]
- 设定当前栈帧的栈底为上一栈帧的栈顶 [movl %esp, %ebp]
- 获取局部变量,并放置于寄存器中。这里我们回忆一下main函数中,局部变量应当是放置于栈顶处以及栈顶地址+4个字节的地址处。而在fooii函数中,+8以及+12是因为当main函数调用fooii函数时在其栈顶处压入了下一指令的执行地址,而在32位系统中这恰好是4个字节。 [movl 12(%ebp), %eax];[movl 8(%ebp), %ecx]
- 保存变量在该栈帧中,而后根据指针偏移量进行addl操作。
- 移动esp指针,完成退栈,运算结果保存在eax寄存器中。弹出上一栈帧的栈基指针。推出函数
结
在讨论完栈帧的生成以及退出后,想必已经对函数调用有了很直观的感受了。
我在此总结如下:
1.函数调用实质是对函数地址的调用。当我们使用calll指令调用函数时,实质是把函数地址置入eip寄存器中。【指令】
2.函数调用伴随调用栈的管理,调用函数生成栈帧,退出函数销毁栈帧。
3.调用函数保存局部变量于栈顶,且保存自己的下一条指令地址。
4.当被调用函数退出,调用函数的下一条指令地址位于当前调用栈的栈顶,弹出后eip回到原函数继续执行calll指令以下的指令。