这个问题很危险地接近过于宽泛和主要基于意见的界限,但我明白你在问什么。
了解历史上存在无数不同的处理器设计和系统实现。语言和处理器随着时间的推移而发展。因此,任何绝对声明实际上都是有限的,因为毫无疑问,该声明不适用于某个系统或处理器。
通常堆栈只是内存,堆栈指针只是该内存中的地址/偏移量,push/pop 与普通内存访问有何不同是程序员通常不/不应该关心的具体情况地址,而是相对的,我推了五件事,所以第三件事离堆栈指针很远,要清理我需要弹出 5 件事,等等。但它只是一个带有地址指针的地方的 ram。
虽然我们认为编号越低的地址越低,编号越大的地址越高,并且期望内存的绘图/可视化具有较低的编号地址在图表上较低且较高的地址在图表上,但有时是有充分理由的或者有时不是这样。在芯片上并没有真正的上升或下降,也没有假设内存是以某种长的物理线性 2D 方式布局的,这些都是简单的可视化。
我不知道有什么异常,但通常处理器以地址递增的方向执行,地址 0x1000 处的一条指令长度为 4 个字节,下一条指令假定位于 0x1004,而不是 0xFFC。所以让我们假设代码向上或从低地址向高地址增长。
假设我们的固件在 ram 中运行,而不是在 flash 中,我们正在谈论 ram 的消耗。并且考虑裸机而不是同时加载许多应用程序的操作系统。
一个程序通常会有一些代码(通常称为 .text)、一些数据、(全局)变量等(通常称为 .data 和 .bss)。堆是运行时分配的内存和堆栈。
我没有对此进行研究,但根据我所教的内容和名称本身,人们可以将 stack 视为一堆盘子或一堆便条卡。由于重力向上生长。并且独立于处理器架构,将堆栈可视化为向上增长的情况并不少见,新项目放置在旧项目之上,移除顶部项目以获得较低项目。但这不是那么严格,不确定它是否是 50/50,但您会经常看到它被可视化为向下和向上增长。或者一个滑动窗口,堆栈指针在图表中没有视觉移动,但数据根据显示方式上下移动。
另外请注意,该站点的名称 Stack Overflow,该术语对它的含义有一个特定的假设......
所以切入正题,经典模型(后面会提到例外情况)是从较低的内存开始,或者我们甚至假设为零,你有你的代码、机器代码以及属于该类别的任何其他内容。然后你有你的全局变量 .data 和 .bss,然后你有你的堆,最上面的是你的堆栈。堆和堆栈在运行时被认为是动态的。如果您从未释放,则假定堆向上增长。所以堆栈的自然解决方案是让它向下增长。你从最低地址开始你的堆,理想情况下你可以在其他项目(.text,.data,.bss)的顶部和尽可能高的堆栈,以便堆栈溢出(堆栈和堆冲突,堆栈增长到堆分配的内存中)。
这种传统模型意味着堆栈向下增长意味着从较高地址到较低地址。许多指令集架构将推送/弹出解决方案限制在此范围内,使用所设计的指令堆栈向下增长存在例外情况,例如传统(aarch64 之前)arm 指令(全尺寸而非拇指)可以采用任何一种方式,因此情况是编译器作者的选择,而不是由架构强制。可以说,使用可以访问内存的通用寄存器,编译器可以选择使用简单的加载/存储指令,而不是推送/弹出或等效指令,然后做任何他们想做的事情。但除了可能非常有限的例外,从地址角度来看,堆栈会向下增长。
一些架构的堆栈被埋在不可见的空间中,旧的旧芯片相对于今天可能有一个非常小的堆栈,比如 16 深或 32 深,我们唯一的访问是推送和弹出,仅此而已。
一些具有 push/pop 或等效功能的架构,例如在 push 时会写入然后调整堆栈指针或调整堆栈指针然后写入,因此对于 16 位系统,您可以从 0x10000 开始获取所有位置您不能代表 0x0000,其他 0xffff 或 0xfffc 取决于架构及其工作方式等。
因此,如果您想将堆栈可视化为实际上是一堆东西,一堆便笺卡,一堆盘子等。然后由于重力,您会将其可视化为向上生长。我在便条卡上写了一个数字,将其放在堆栈上 在便条卡上写下另一个数字,然后将其放置(推)到堆栈上,取出卡(弹出)等等。因此,由于它是一个 50/50 的东西,您有时会看到堆栈以这种方式可视化,较高的地址位于图表的下部,较低的地址位于图表的上部。
因此基于意见,这就是他们以这种方式绘制图表的原因。归根结底,要做好心理准备,以应对人们想象堆栈的任何方式。
- 为什么堆栈指针从堆栈中的最后一个地址开始?
这是经典意义上的典型。但是,在现实世界中,有些用例将堆栈放置在与其他项目不同的内存空间中,这些项目可能会受到安全功能(mmu 等)的保护而不会超出其空间。但是,堆栈指针和/或指令的正常使用是堆栈相对于所使用的内存地址向下增长,这通常是一个架构限制。所以如果你长大了,你想从高处开始。最后一个地址是一种教科书式的方法,但您经常会看到人们在链接描述文件中分配堆栈空间,然后它就在它所在的位置(有时甚至在堆或数据之下)。
- 这真的是在所有语言中实现堆栈的方式吗?
太宽泛了,语言本身编译成的代码使用指令、链接和引导程序(或操作系统)来确定程序堆栈的初始值。基于堆栈指针的指令被限制为向下增长的堆栈并不少见。如果有基于意见的选择,我预计由于历史原因,实施将向下(地址)增长。
- 这种实现堆栈的方式是否有助于避免因堆栈溢出而出现的问题?
是的,如果我们假设堆向上增长而堆栈向下增长,那么您希望堆从可用空间的底部开始,而顶部的堆栈在堆栈溢出发生之前提供最大的空间。
- 和栈和堆在内存中的存储方式有关系吗?
是的,基于意见。如上所述。
- 如果我们从地址 $ffe6 开始会发生什么变化?
实际上,每个“函数”都被称为堆栈指针所在的位置,这就是您不关心地址的全部意义,只关心匹配推送和弹出或可能的相对寻址,而不是绝对的。因此,如果 $ffe6 那么当您推送和弹出地址时,地址会变小/变大。如果 8000 美元,同样的交易 5432 美元,同样的交易。如果您从与教程中显示的地址不同的地址开始,一切都一样,只是显示的物理地址需要反映新的起点。
所以是的,堆栈的传统/教科书视图是后进先出的。地址空间向下增长,但 50/50 的文本作者如何在图表底部或顶部以高地址可视化这一点。实际上,更高性能的指令集不仅限于严格的推送和弹出,还包括相对寻址,所以当你开始学习推送/弹出时,你就可以直接进入相对寻址。我将 5 个东西压入堆栈,我可以使用 sp+offset 寻址访问所有这些东西,有时使用基于特殊 sp 的指令。
不要为某些教程/教科书作者如何可视化堆栈、顶部或底部的更高地址而烦恼。