在大多数现代系统中,堆栈增长的方向是什么?


Answers:


147

堆栈的增长通常不取决于操作系统本身,而是取决于运行的处理器。例如,Solaris在x86和SPARC上运行。如前所述,Mac OSX在PPC和x86上运行。Linux可以运行在从我的大型honkin System z到微不足道的小手表等所有东西上。

如果CPU提供任何选择,则操作系统要使用的ABI /调用约定指定了您希望代码调用所有人的代码时需要做出的选择。

处理器及其方向是:

  • x86:下
  • SPARC:可选。标准的ABI用尽。
  • PPC:下来,我想。
  • System z:在一个链表中,我没有骗你(但至少在zLinux上还是这样)。
  • ARM:可选,但是Thumb2仅具有向下压缩的紧凑编码(LDMIA =之后递增,STMDB =之前递减)。
  • 6502:向下(但仅256个字节)。
  • RCA 1802A:可以采用任何方式,但要视SCRT的实施而定。
  • PDP11:关闭。
  • 8051:向上。

为了显示我的年龄,1802是用来控制早期航天飞机的芯片(基于它的处理能力,我怀疑门是否打开了:-)和第二台计算机COMX-35(按照我的ZX80)。

PDP11细节从这里收集,8051细节从这里收集。

SPARC体系结构使用滑动窗口寄存器模型。架构上可见的详细信息还包括寄存器窗口的循环缓冲区,该缓冲区有效并在内部缓存,并且在上溢/下溢时会出现陷阱。有关详细信息,请参见此处。如SPARCv8手册所述,SAVE和RESTORE指令类似于ADD指令加上寄存器窗口旋转。使用正常数而不是通常的负将产生向上增长的堆栈。

前面提到的SCRT技术是另一种技术-1802使用了一部分或16个16位寄存器用于SCRT(标准调用和返回技术)。一个是程序计数器,您可以将任何寄存器与SEP Rn指令一起用作PC 。一个是堆栈指针,两个始终设置为指向SCRT代码地址,一个用于调用,一个用于返回。没有对寄存器进行特殊处理。请记住,这些细节来自内存,它们可能并不完全正确。

例如,如果R3是PC,R4是SCRT调用地址,R5是SCRT返回地址,R2是“堆栈”(引用是在软件中实现的),SEP R4则将R4设置为PC并开始运行SCRT通话代码。

然后它将R3存储在R2“堆栈”上(我认为R6用于临时存储),向上或向下进行调整,获取R3之后的两个字节,将它们加载 R3中,然后SEP R3在新地址处运行。

要返回,它将SEP R5从R2堆栈中拉出旧地址,向其中添加两个(跳过调用的地址字节),将其加载到R3中并SEP R3开始运行先前的代码。

最初,在完成所有基于6502/6809 / z80堆栈的代码之后,很难把头缠起来,但是仍然以一种“碰头撞墙”的方式保持优雅。该芯片的最大卖点之一是全套的16个16位寄存器,尽管事实上您立即丢失了其中的7个(5个用于SCRT,两个用于DMA和内存中断)。啊,营销胜过现实:-)

系统z实际上非常相似,使用其R14和R15寄存器进行呼叫/返回。


3
要添加到列表中,ARM可以向任一方向扩展,但可以通过特定的芯片实现方式设置为一个或另一个(或可以通过软件选择)。我处理过的少数几个始终处于成长模式。
Michael Burr

1
到目前为止,在ARM世界的一小部分(ARM7TDMI)中,堆栈完全由软件处理。返回地址存储在寄存器中,如果需要,可以通过软件将其保存,并且递增/递减指令前后的指令允许将其和其他内容沿任一方向放入堆栈中。
starblue

1
一个HPPA,堆栈长大了!在相当现代的建筑中很少见。
tml

2
出于好奇,这里提供了有关堆栈如何在z / OS上工作的好资源:www-03.ibm.com/systems/resources/Stack+and+Heap.pdf
Dillon

1
感谢@paxdiablo的理解。有时,当您发表这样的评论时,尤其是当它是一个较旧的评论时,人们会将其视为个人的侮辱。我只知道有区别,因为我过去自己犯过同样的错误。照顾自己。
CasaDeRobison

23

在C ++(适用于C)stack.cc中

static int
find_stack_direction ()
{
    static char *addr = 0;
    auto char dummy;
    if (addr == 0)
    {
        addr = &dummy;
        return find_stack_direction ();
    }
    else
    {
        return ((&dummy > addr) ? 1 : -1);
    }
}

14
哇,距离我看到“ auto”关键字已有很长时间了。
paxdiablo

9
(&dummy> addr)未定义。仅当两个指针指向同一数组或结构内时,才定义将两个指针馈给关系运算符的结果。
sigjuice

2
尝试研究自己的堆栈的布局-C / C ++完全没有指定的内容-最初是“不可移植的”,因此我并不在乎。看来此功能只能正常运行一次。
迅速

9
无需为此使用static。相反,您可以将地址作为参数传递给递归调用。
R .. GitHub STOP HELPING ICE

5
另外,通过使用static,如果您多次调用此命令,则后续调用可能会失败...
克里斯·多德

7

向下扩展的优势在于,在较旧的系统中,堆栈通常位于内存的顶部。程序通常从底部开始填充内存,因此这种内存管理可最大程度地减少测量堆栈底部并将其放置在合理位置的需求。


3
不是“优势”,而是重言式。
罗恩侯爵,

1
不是重言式。关键是要有两个正在增长的内存区域不干扰(除非内存反正已满),如@valenok所指出的。
YvesgereY

6

堆栈在x86上变小(由体系结构定义,pop递增堆栈指针,push递减。)


5

在MIPS和许多现代的RISC体系结构(如PowerPC上,RISC-V,SPARC ...)有没有pushpop说明。这些操作是通过手动调整堆栈指针,然后相对于调整后的指针加载/存储值来明确完成的。所有寄存器(零寄存器除外)都是通用的,因此理论上任何寄存器都可以是堆栈指针,并且堆栈可以按程序员希望的任何方向增长

就是说,在大多数体系结构上,堆栈通常会变小,这可能是为了避免堆栈和程序数据或堆数据变大并相互冲突的情况。sh-的答案中也提到了很多解决原因。一些示例:MIPS ABI向下增长并使用$29(AKA $sp)作为堆栈指针,RISC-V ABI也向下增长并使用x2作为堆栈指针

在Intel 8051中,堆栈长大了,可能是因为内存空间非常小(原始版本中为128字节)以至于没有堆,而且您无需将堆栈放在顶部,以便将其与堆长分开从下往上

您可以在https://en.wikipedia.org/wiki/Calling_convention中找到有关各种体系结构中堆栈使用情况的更多信息。

也可以看看


2

仅是其他答案的一小部分,据我所知,这还没有涉及到这一点:

使堆栈向下生长会使堆栈内的所有地址相对于堆栈指针具有正偏移。不需要负偏移,因为它们只会指向未使用的堆栈空间。当处理器支持相对于堆栈指针的寻址时,这简化了对堆栈位置的访问。

许多处理器的指令允许相对于某些寄存器仅以正偏移量进行访问。这些建筑包括许多现代建筑以及一些旧建筑。例如,ARM Thumb ABI通过在单个16位指令字中编码的正偏移量来提供相对于堆栈指针的访问。

如果堆栈向上增长,则相对于堆栈指针的所有有用偏移都将为负,这将使直观性和便利性降低。与寄存器相对寻址的其他应用程序(例如用于访问结构的字段)的应用程序也不一致。



1

它之所以减少,是因为分配给程序的内存具有“永久数据”,即程序本身的代码在底部,然后是堆在中间。您需要另一个固定点来引用堆栈,以便从顶部开始。这意味着堆栈会逐渐变小,直到可能与堆上的对象相邻为止。


0

此宏应在没有UB的运行时对其进行检测:

#define stk_grows_up_eh() stk_grows_up__(&(char){0})
_Bool stk_grows_up__(char *ParentsLocal);

__attribute((__noinline__))
_Bool stk_grows_up__(char *ParentsLocal) { 
    return (uintptr_t)ParentsLocal < (uintptr_t)&ParentsLocal;
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.