2019年11月22日 / 4次阅读
CC++
不知道从什么时候开始,运行在OS内的程序文件,有了规范,Win系统下是PE,Linux系统下是ELF。符合规范的程序能够被OS调用并执行。
不知道从什么时候开始,程序在OS的管控下执行,每个进程都有自己的堆(heap)和栈(stack)。堆的地址从小到大,存放需要程序代码显式申请的内存块,并且也需要程序自己管理和释放。栈的地址从大到小,存放函数调用过程中的指令地址和函数的局部变量。程序员需要关注堆的管理,但是栈的相关操作,由编译器完成。
在栈中存放程序返回的地址,这个是很好理解的。在栈中存放函数的局部变量,并且在函数执行完后释放这部分空间,我理解这是当初的一个很精妙的软件设计。与全局变量存放在.data数据区不同,函数仅自己使用的变量由于只有短暂的生命周期,如果使用像堆一样显式地申请和释放的方式,必然会增加程序员的代码工作量,也会增加软件出错的概率。因此,这些函数自己使用的变量,就放在了栈中,由编译器自动完成在栈中的空间申请,赋初值和释放的动作。
CPU很配合,对于这些已经成为标准的软件设计概念,贡献出对应的寄存器来支持!
刚才是一点个人的牢骚,本文后面主要记录一些关于函数调用栈的知识点。
关于压栈出栈的不同方式
对于函数调用时的压栈和出栈,其实在细节上有一些不同的实现方式,如下图,据说是VC++的标准:
几种不同的压栈出栈方式
C的标准,应该就是从右到左压栈,这样的效果是,最右边的参数地址,在高地址。
一个函数调用压栈出栈的具体细节
函数调用大概包括以下几个步骤:
(1)参数入栈:将参数从右向左依次压入系统栈中。
(2)返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行。
(3)代码区跳转:处理器从当前代码区跳转到被调用函数的入口处。
(4)栈帧调整:具体包括:
<1>保存当前栈帧状态值,已备后面恢复本栈帧时使用(EBP入栈)。
<2>将当前栈帧切换到新栈帧(将ESP值装入EBP,更新栈帧底部)。
<3>给新栈帧分配空间(把ESP减去所需空间的大小,抬高栈顶)。
<4>对于_stdcall调用约定,函数调用时用到的指令序列大致如下:
push 参数3 ;假设该函数有3个参数,将从右向做依次入栈
push 参数2
push 参数1
call 函数地址 ;call指令将同时完成两项工作:a)向栈中压入当前指令地址的下一个指令地址,即保存返回地址。 b)跳转到所调用函数的入口处。
push ebp ;保存旧栈帧的底部
mov ebp,esp ;设置新栈帧的底部 (栈帧切换)
sub esp,xxx ;设置新栈帧的顶部 (抬高栈顶,为新栈帧开辟空间)
函数返回的步骤如下:
<1>保存返回值,通常将函数的返回值保存在寄存器EAX中。
<2>弹出当前帧,恢复上一个栈帧。具体包括:
(1)在堆栈平衡的基础上,给ESP加上栈帧的大小,降低栈顶,回收当前栈帧的空间。
(2)将当前栈帧底部保存的前栈帧EBP值弹入EBP寄存器,恢复出上一个栈帧。
(3)将函数返回地址弹给EIP寄存器。
<3>跳转:按照函数返回地址跳回母函数中继续执行。
还是以C语言和Win32平台为例,函数返回时的相关的指令序列如下:
add esp,xxx ;降低栈顶,回收当前的栈帧
pop ebp ;将上一个栈帧底部位置恢复到ebp
retn ;a)弹出当前栈顶元素,即弹出栈帧中的返回地址,至此,栈帧恢复到上一个栈帧工作完成。b)让处理器跳转到弹出的返回地址,恢复调用前代码区
一个有函数局部变量示意的调用栈图
函数调用栈
系统栈
OS负责不同进程的切换,每个进程都有自己的栈,编译好的程序,都能自己管理好自己的栈。
CPU层面也有栈,这个栈是系统栈。比如在有中断的时候,CPU也需要跳转到其它地方去执行别的指令,这时也需要压栈和出栈。压栈保存CPU执行现场,即各种寄存器的值;出栈就是恢复执行现场。对应的机器指令有call,ret,push和pop。
注意CPU的各类jmp指令,它就是条件跳转,与压栈出栈无关。因为各种jmp指令,使得我们可以直接在高级语言中实现各种程序执行控制。
以上就是本人对函数调用栈和函数的局部变量的一点总结。
本文链接:https://www.maixj.net/ict/local-stack-23097
前一篇:只有在正确的地方,才会产生正确的价值
后一篇:头脑风暴与独立思考
©Copyright 麦新杰 Since 2014 云上小悟独立博客版权所有 备案号:苏ICP备14045477号-1。云上小悟网站部分内容来源于网络,转载目的是为了整合信息,收藏学习,服务大家,有些转载内容也难以判断是否有侵权问题,如果侵犯了您的权益,请及时联系站长,我会立即删除。