堆栈如何在其中存储值?

How does a stack stores values in it?

我正在观看视频 What is stack and why do we need it?,这是讲师向我们展示如何在 Javascript 中制作 16 位 VM 的课程的一部分。

我的问题来自视频的其中一帧中堆栈的表示。

让我快速向大家介绍一下这个框架中发生的事情。在图像的左侧,我们有所有寄存器:

在图像的右上部分,我们有接下来要执行的指令。下面是堆栈的当前状态。

现在,如图所示,sp 当前指向地址 $fffa,对应的值为 [=18=]00。在此之前,sp 指向地址$fffc,要执行的指令是psh r2。同样,在此之前,sp 指向地址 $fffe 并且指令是 psh r1.

现在,我在整个场景中不明白的是地址会随着我们向下移动而增加,例如上面的地址是$ffe6,下面的地址是$ffe8,明显比它的前身大,地址还在不断增加。但是为什么我们的堆栈指针 sp 首先从地址 $fffe 开始呢?我知道堆栈以 FILO(先进后出)/LIFO(后进先出) 方式工作,如果我们忽略地址的顺序,我们将遵循 FILO/LIFO。但让我困惑的是地址的顺序。如果我们将堆栈逆时针旋转 90 度,那么我们的堆栈将看起来像这样。

为什么我旋转堆栈是为了了解内存地址在任何体系结构中的布局方式。

现在,我有这些问题。

  1. 为什么堆栈指针从堆栈中的最后一个地址开始?
  2. 这真的是所有语言中堆栈的实现方式吗?
  3. 这种实现堆栈的方式是否有助于避免由于堆栈溢出而出现的问题?
  4. 跟栈和堆在内存中的存储方式有关系吗?
  5. 如果我们从地址 $ffe6 开始,会有什么变化?

我可能在一个 post 中问了很多问题,但请您最注意最后一个问题,因为我认为它可能会回答所有其他问题。谢谢。

这个问题过于宽泛和主要基于观点的界限非常危险,但我明白你在问什么。

了解历史上有无数不同的处理器设计和系统实现。随着时间的推移,语言和处理器也在不断发展。因此,任何绝对陈述实际上都是有限的,因为毫无疑问,该陈述不适用于某个系统或处理器。

通常堆栈只是内存,堆栈指针只是该内存中的一个 address/offset,push/pop 与普通内存访问的不同之处在于程序员没有't/shouldn 通常不关心具体地址,而是相对的,我推了五个东西,所以第三个东西离堆栈指针这么远,为了清理我需要弹出 5 个东西,等等。但它只是某个地方的 ram地址指针。

虽然我们认为低编号地址是低编号地址,高编号地址是高编号地址,并且期望 drawings/visualizations 内存的低编号地址在图表中较低,而高地址在图表中较高,但有时出于充分的理由或有时不是,这被翻转了。在芯片上并没有真正的上升或下降,也没有假设内存是以某种长的物理线性二维方式布局的,这些都是简单的可视化。

我不知道有没有例外,但通常处理器会按地址递增的方向执行,地址为 0x1000 的指令长度为 4 个字节,下一条指令假定为 0x1004,而不是 0xFFC。因此,让我们假设代码向上增长或从低地址增长到高地址。

让我们假设我们的固件 运行s 在 ram 而不是闪存中,我们正在谈论 ram 的消耗。并从裸机的角度思考,而不是一次加载许多应用程序的操作系统。

一个程序一般都会有一些代码(通常称为.text),一些数据,(全局)变量等(通常称为.data和.bss)。堆是 运行 时间分配的内存和堆栈。

我没有对此进行研究,但根据我所学的内容和名称本身,可以将堆栈视为一叠盘子或一叠记事卡。由于重力而向上生长。独立于处理器架构,将堆栈可视化为向上增长并不少见,新项目放置在旧项目之上,移除顶部项目以获取较低项目。但这并不是那么死板,不确定它是否是 50/50,但你会经常看到它可视化为向下和向上增长。或滑动 window,堆栈指针在图表中没有视觉移动,但数据根据显示方式向上或向下移动。

另请注意,该站点的名称 Stack Overflow,该术语具有特定的含义假设...

切入正题,经典模型(后面会提到例外情况)是从较低的内存开始,或者我们甚至假设为零,你有你的代码、机器代码以及属于该类别的任何其他内容.然后你有你的全局变量.data 和.bss,然后你有你的堆,最顶层是你的堆栈。堆和堆栈在 运行 时被认为是动态的。如果您从不释放,则假定堆向上增长。所以 stack 的自然解决方案是让它向下增长。您从最低地址开始堆,理想情况下是在其他项目(.text、.data、.bss)之上,堆栈尽可能高,以便堆栈溢出(堆栈和堆冲突,堆栈增长进入堆分配的 ram)。

这种传统模型意味着堆栈向下增长,即从高地址到低地址。许多指令集架构限制了 push/pop 解决方案,使用设计的指令堆栈向下增长有例外,例如传统的(pre-aarch64)arm 指令(全尺寸而不是拇指)可以走任何一条路,所以在那种情况下,这是编译器作者的选择,而不是受体系结构的强制。可以说,使用可以访问内存的通用寄存器,编译器可以选择使用简单的 load/store 指令而不是 push/pop 或等效指令,并做他们想做的任何事情。但除了可能非常有限的例外情况,堆栈从地址的角度向下增长。

一些架构的堆栈隐藏在不可见的 space 中,旧的旧芯片可能相对于今天有一个非常小的堆栈,如 16 深或 32,我们唯一的访问是推送和弹出,仅此而已。

一些具有 push/pop 或等价物的体系结构,例如在推送时将写入然后调整堆栈指针或调整堆栈指针然后写入,因此对于 16 位系统,您可以获取所有位置将以 0x10000 开头,您不能代表 0x0000,其他 0xffff 或 0xfffc 取决于体系结构及其工作方式等。

所以如果你想把一堆想象成一堆东西,一堆便条卡,一堆盘子,等等。然后由于重力,你会把它想象成向上生长。我在记事卡上写一个数字,把它放在堆栈上,在记事卡上写另一个数字,然后把它放在(推)堆栈上,取出卡片(弹出)等等。因此,由于它是 50/50 的比例,您有时会看到以这种方式可视化的堆栈,其中较高的地址位于图表的下部,而较低的地址位于图表的上部。

基于意见,这就是他们那样绘制图表的原因。在一天结束时,请做好心理准备,以处理人们想象堆栈的任何方式。

  1. 为什么堆栈指针从堆栈中的最后一个地址开始?

这是经典意义上的典型。不过,在现实世界中,有些用例中堆栈与其他可能受到安全功能(mmu 等)保护而不会超出其 space 的项目放置在不同的内存 space 中。但堆栈指针 and/or 指令的正常使用通常是一种架构限制,因为堆栈相对于所使用的内存地址向下增长。所以,如果你长大了,你想从高处开始。最后一个地址是一种教科书式的方法,但您经常会看到人们在链接描述文件中分配堆栈 space 并且它到达它到达的地方(有时甚至在堆或数据下方)。

  1. 这真的是所有语言中堆栈的实现方式吗?

太宽泛了,语言本身编译成使用指令的代码,它的链接和决定程序堆栈初始值的bootstrap(或操作系统)。基于堆栈指针的指令被限制为向下增长的堆栈并不少见。如果有选择,基于意见,我希望由于历史的原因,实施将向下(地址)增长。

  1. 这种实现堆栈的方式是否有助于避免由于堆栈溢出而出现的问题?

是的,如果我们假设堆向上增长而堆栈向下增长,那么您希望堆从可用空间的底部开始 space 并且堆栈在顶部以在堆栈之前提供最多的空间发生溢出。

  1. 跟栈和堆在内存中的存储方式有关系吗?

是的,基于意见。如上所述。

  1. 如果我们从地址 $ffe6 开始,会有什么变化?

实际上没什么,因为每个“函数”都被称为堆栈指针,这就是您不关心地址的全部要点,只关心匹配推送和弹出或可能的相对寻址,而不是绝对寻址.因此,如果 $ffe6 那么当你推送和弹出地址时,地址将变为 smaller/larger。如果 8000 美元,同样的交易 5432 美元,同样的交易。如果您的起始地址与教程中显示的地址不同,一切都一样,只是显示的物理地址需要反映新的起点。


所以是的,堆栈的 traditional/textbook 视图是后进先出的。地址 space 向下增长,但 50/50 关于文本的作者如何在图表底部或顶部用高地址可视化它。实际上,更高性能的指令集不仅限于严格的压入和弹出,还包括相对寻址,因此当您从学习 push/pop 开始时,您可以直接进入相对寻址。我将 5 个东西压入堆栈,我可以使用 sp+offset 寻址访问所有这些东西,有时使用基于特殊 sp 的指令。

不要为某些 tutorial/textbook 作者如何可视化堆栈、顶部或底部的更高地址而烦恼。