为啥堆栈通常会向下增长?
Posted
技术标签:
【中文标题】为啥堆栈通常会向下增长?【英文标题】:Why do stacks typically grow downwards?为什么堆栈通常会向下增长? 【发布时间】:2011-01-03 09:10:05 【问题描述】:我知道在我个人熟悉的架构(x86、6502 等)中,堆栈通常会向下增长(即,压入堆栈的每个项目都会导致 SP 递减,而不是递增)。
我想知道这其中的历史原因。我知道在统一的地址空间中,在数据段的另一端(比如说)开始堆栈很方便,所以如果两侧在中间发生冲突,就会出现问题。但是为什么堆栈传统上会获得顶部?特别是考虑到这与“概念”模型有何不同?
(请注意,在 6502 架构中,堆栈也会向下增长,即使它被限制为单个 256 字节的页面,而且这个方向选择似乎是任意的。)
【问题讨论】:
【参考方案1】:因为POP
使用与通常用于扫描字符串和数组相同的寻址模式
从堆栈中弹出值的指令需要做两件事:从内存中读取值,并调整堆栈指针。此操作有四种可能的设计选择:
预增量堆栈指针,然后读取值。这意味着堆栈将“向下”增长(朝向较低的内存地址)。
预减堆栈指针,然后读取值。这意味着堆栈将“向上”增长(朝向更高的内存地址)。
先读取值,然后后增量堆栈指针。这意味着堆栈将向下增长。
先读取值,然后postdecrement堆栈指针。这意味着堆栈将向上增长。
在许多计算机语言(尤其是 C)中,字符串和数组作为指向其第一个元素的指针传递给函数。一个很常见的操作是从第一个元素开始按顺序读取字符串或数组的元素。这样的操作只需要上面描述的postincrement寻址方式。
此外,读取字符串或数组的元素比写入元素更常见。事实上,有许多标准库函数根本不执行任何写入操作(例如strlen()
、strchr()
、strcmp()
)!
因此,如果您的指令集设计中的寻址模式数量有限,那么最有用的寻址模式将是读取后增量。这不仅产生了最有用的字符串和数组操作,而且产生了一个向下增长堆栈的POP
指令。
第二个最有用的寻址模式是后递减写入,可用于匹配的PUSH
指令。
确实,PDP-11 had postincrement and predecrement 寻址模式产生了向下增长的堆栈。甚至VAX 也没有前置或后置。
【讨论】:
【参考方案2】:在最小的嵌入式系统中,递减堆栈增长的一个优点是单个 RAM 块可以冗余映射到页面 O 和页面 1,允许从 0x000 开始分配零页变量,并且堆栈从 0x1FF 向下增长,在覆盖变量之前最大化它必须增长的数量。
6502 最初的设计目标之一是它可以与例如 6530 结合,从而形成一个具有 1 KB 程序 ROM、定时器、I/O 和 64 字节的双芯片微控制器系统堆栈和页面零变量之间共享的 RAM。相比之下,当时基于 8080 或 6800 的最小嵌入式系统将是四五个芯片。
【讨论】:
【参考方案3】:再多 2c:
除了提到的所有历史理由之外,我很确定在现代处理器中没有任何理由有效。所有处理器都可以采用带符号的偏移量,并且自从我们开始处理多个线程以来,最大化堆/堆栈距离就没有实际意义了。
我个人认为这是一个安全设计缺陷。比如说,如果 x64 架构的设计者改变了堆栈的增长方向,那么大多数堆栈缓冲区溢出都会被消除——这是一件大事。 (因为字符串向上增长)。
【讨论】:
【参考方案4】:Stanley Mazor(4004 和 8080 架构师)在"Intel Microprocessors: 8008 to 8086" 中解释了如何为 8080(以及最终为 8086)选择堆栈增长方向:
堆栈指针被选择为“下坡”运行(堆栈向低位内存前进)以简化从用户程序到堆栈的索引(正索引)并简化从前面板显示堆栈内容。
【讨论】:
【参考方案5】:至于历史的基本原理,我不能肯定地说(因为它们不是我设计的)。我对此事的想法是,早期的 CPU 将其原始程序计数器设置为 0,并且很自然地希望从另一端开始堆栈并向下增长,因为它们的代码自然会向上增长。
顺便说一句,请注意,在复位时将程序计数器设置为 0 并不是 所有 早期 CPU 的情况。例如,摩托罗拉 6809 将从地址
0xfffe/f
获取程序计数器,因此您可以在任意位置开始运行,具体取决于该地址提供的内容(通常但不限于 ROM)。
一些历史系统会做的第一件事是从顶部扫描内存,直到找到一个可以读回写入相同值的位置,以便它知道实际安装的 RAM(例如,z80 与64K 地址空间不一定有 64K 或 RAM,事实上 64K 在我早期就已经大量了)。一旦找到顶部实际地址,它将适当地设置堆栈指针,然后可以开始调用子例程。这种扫描通常由 CPU 在 ROM 中运行代码作为启动的一部分来完成。
关于栈的增长,并不是所有的都是向下增长的,详见this answer。
【讨论】:
我喜欢 Z80 RAM 检测策略的故事。文本段的布局是向上增长的,这在一定程度上是有道理的——过去的程序员在处理其含义方面比堆栈更直接。谢谢 paxdiablo。指向堆栈实现的替代形式集的指针也非常有趣。 早期内存不是有办法通知它的大小,我们必须手动计算吗? @LưuVĩnhPhúc,我不得不假设你比我落后一代(或两代)。我仍然记得 TRS-80 模型 3 获取日期和时间的方法是在启动时向用户询问。使用内存扫描器来设置内存上限在当时被认为是最先进的 :-) 你能想象如果 Windows 在每次启动时询问时间或你有多少内存会发生什么吗?跨度> 确实,Zilog Z80 文档说该部件通过将 PC 寄存器设置为 0000h 并执行来启动。它将中断模式设置为 0,禁用中断,并将 I 和 R 寄存器也设置为 0。之后,它开始执行。在 0000h,它开始运行代码。该代码必须先初始化堆栈指针,然后才能调用子程序或启用中断。哪家供应商销售的 Z80 的行为方式与您描述的一样? 迈克,对不起,我应该更清楚。当我说 CPU 扫描内存时,我并不是说这是 CPU 本身的特性。它实际上是由 ROM 中的程序控制的。我会澄清的。【参考方案6】:我相信约定始于 IBM 704 及其臭名昭著的“递减寄存器”。现代语言将其称为指令的偏移字段,但重点是它们向下,不 向上。
【讨论】:
【参考方案7】:我不确定,但我以前为 VAX/VMS 做过一些编程。我似乎记得内存的一部分(堆??)上升而堆栈下降。当两个人相遇时,你已经失忆了。
【讨论】:
这是真的,但是为什么堆是向上增长而不是相反呢? ***.com/questions/2035568/…【参考方案8】:我相信这纯粹是一个设计决定。并非所有这些都向下增长——请参阅this SO thread,了解有关不同架构上堆栈增长方向的一些很好的讨论。
【讨论】:
【参考方案9】:一个可能的原因可能是它简化了对齐。如果您在堆栈上放置一个必须放置在 4 字节边界上的局部变量,您可以简单地从堆栈指针中减去对象的大小,然后将两个低位清零以获得正确对齐的地址。如果堆栈向上增长,确保对齐变得有点棘手。
【讨论】:
计算机不会减法;他们添加了 2 的赞美。通过减法完成的任何事情实际上都是通过加法完成的。考虑一下,计算机有加法器,而不是减法器。 @jww - 这是一个没有区别的区别。我可能会声称计算机不添加它们只会减去!出于此答案的目的,这并不重要 - 但大多数 ALU 将使用a circuit,它支持加法和减法并具有相同的性能。也就是说,虽然A - B
在概念上可以实现为 A + (-B)
(即,B
的单独否定步骤),但实际上并没有。
@jww 你的挑剔对于早期的计算机来说是错误的 - 二进制补码需要一些时间才能获胜,直到它成功之前,有些计算机使用了一个补码和符号和大小,也许还有其他东西.通过这些实现,加法与减法很可能有优势。因此,在没有其他信息的情况下,将其排除为影响寻址方案选择(如堆栈方向)的可能因素是错误的。【参考方案10】:
我听到的一个很好的解释是,过去有些机器只能有无符号偏移量,所以你希望堆栈向下增长,这样你就可以在不丢失额外指令来伪造负偏移量的情况下击中本地人.
【讨论】:
【参考方案11】:IIRC 堆栈向下增长,因为堆向上增长。情况可能正好相反。
【讨论】:
向上增长的堆在某些情况下可以实现高效的重新分配,但向下增长的堆几乎不会。 @PeterCordes 为什么? @Yashas:因为realloc(3)
需要更多空间 一个对象来扩展映射而不复制。当同一对象后跟任意数量的未使用空间时,可以重复重新分配同一对象。以上是关于为啥堆栈通常会向下增长?的主要内容,如果未能解决你的问题,请参考以下文章