大多数现代系统中堆栈增长的方向是什么?
我正在准备一些 C 语言培训材料,我希望我的示例适合典型的堆栈模型。
Linux、Windows、Mac OSX(PPC 和 x86)、Solaris 和最新的 Unix 中的 C 堆栈朝什么方向发展?
I am preparing some training materials in C and I want my examples to fit the typical stack model.
What direction does a C stack grow in Linux, Windows, Mac OSX (PPC and x86), Solaris, and most recent Unixes?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(9)
堆栈增长通常并不取决于操作系统本身,而是取决于其运行的处理器。 例如,Solaris 在 x86 和 SPARC 上运行。 Mac OSX(正如您所提到的)在 PPC 和 x86 上运行。 Linux 可以在一切设备上运行,从工作中的 System z 到 微不足道的小手表。
如果 CPU 提供任何类型的选择,那么如果您希望自己的代码调用其他人的代码,则操作系统使用的 ABI/调用约定会指定您需要做出的选择。
处理器及其方向是:
1802 是用于控制早期航天飞机的芯片(我怀疑,根据它的处理能力来感应门是否打开,这表明了我的年龄。:-)和我的第二台计算机,COMX-35(遵循我的ZX80)。
PDP11 详细信息从此处收集,8051 详细信息来自此处。
SPARC架构使用滑动窗口寄存器模型。 架构上可见的细节还包括寄存器窗口的循环缓冲区,该缓冲区有效并在内部缓存,在上溢/下溢时带有陷阱。 有关详细信息,请参阅此处。 正如SPARCv8 手册所解释的,SAVE 和 RESTORE 指令就像 ADD 指令加上寄存器窗口旋转。 使用正常数而不是通常的负常数将产生向上增长的堆栈。
上述 SCRT 技术是另一种技术 - 1802 使用一些或十六个 16 位寄存器进行 SCRT(标准调用和返回技术)。 一是程序计数器,您可以使用
SEP Rn
指令将任何寄存器用作PC。 一个是堆栈指针,两个总是指向 SCRT 代码地址,一个用于调用,一个用于返回。 没有寄存器以特殊方式处理。 请记住,这些细节来自记忆,它们可能不完全正确。例如,如果 R3 是 PC,R4 是 SCRT 调用地址,R5 是 SCRT 返回地址,R2 是“堆栈”(在软件中实现时引用),
SEP R4
将设置 R4成为 PC 并开始运行 SCRT 调用代码。然后它将 R3 存储在 R2“堆栈”上(我认为 R6 用于临时存储),向上或向下调整它,抓取 R3 之后的两个字节,将它们加载到 R3 中,然后执行 < code>SEP R3 并在新地址运行。
返回时,
SEP R5
会将旧地址从 R2 堆栈中拉出,向其中添加两个(以跳过调用的地址字节),然后将其加载到 R3 和SEP R3 中
开始运行前面的代码。在完成所有基于 6502/6809/z80 堆栈的代码之后,一开始很难理解,但仍然以一种令人头晕目眩的方式优雅。 该芯片的一大卖点是全套 16 个 16 位寄存器,尽管您立即丢失了其中 7 个(5 个用于 SCRT,两个用于 DMA 和内存中断)。 啊,营销战胜了现实:-)
System z 实际上非常相似,使用 R14 和 R15 寄存器进行调用/返回。
Stack growth doesn't usually depend on the operating system itself, but on the processor it's running on. Solaris, for example, runs on x86 and SPARC. Mac OSX (as you mentioned) runs on PPC and x86. Linux runs on everything from my big honkin' System z at work to a puny little wristwatch.
If the CPU provides any kind of choice, the ABI / calling convention used by the OS specifies which choice you need to make if you want your code to call everyone else's code.
The processors and their direction are:
Showing my age on those last few, the 1802 was the chip used to control the early shuttles (sensing if the doors were open, I suspect, based on the processing power it had :-) and my second computer, the COMX-35 (following my ZX80).
PDP11 details gleaned from here, 8051 details from here.
The SPARC architecture uses a sliding window register model. The architecturally visible details also include a circular buffer of register-windows that are valid and cached internally, with traps when that over/underflows. See here for details. As the SPARCv8 manual explains, SAVE and RESTORE instructions are like ADD instructions plus register-window rotation. Using a positive constant instead of the usual negative would give an upward-growing stack.
The afore-mentioned SCRT technique is another - the 1802 used some or it's sixteen 16-bit registers for SCRT (standard call and return technique). One was the program counter, you could use any register as the PC with the
SEP Rn
instruction. One was the stack pointer and two were set always to point to the SCRT code address, one for call, one for return. No register was treated in a special way. Keep in mind these details are from memory, they may not be totally correct.For example, if R3 was the PC, R4 was the SCRT call address, R5 was the SCRT return address and R2 was the "stack" (quotes as it's implemented in software),
SEP R4
would set R4 to be the PC and start running the SCRT call code.It would then store R3 on the R2 "stack" (I think R6 was used for temp storage), adjusting it up or down, grab the two bytes following R3, load them into R3, then do
SEP R3
and be running at the new address.To return, it would
SEP R5
which would pull the old address off the R2 stack, add two to it (to skip the address bytes of the call), load it into R3 andSEP R3
to start running the previous code.Very hard to wrap your head around initially after all the 6502/6809/z80 stack-based code but still elegant in a bang-your-head-against-the-wall sort of way. Also one of the big selling features of the chip was a full suite of 16 16-bit registers, despite the fact you immediately lost 7 of those (5 for SCRT, two for DMA and interrupts from memory). Ahh, the triumph of marketing over reality :-)
System z is actually quite similar, using its R14 and R15 registers for call/return.
C++ 语言(适用于 C)stack.cc:
In C++ (adaptable to C) stack.cc:
只是对其他答案的一个小补充,据我所知,还没有触及这一点:
让堆栈向下增长使得堆栈中的所有地址相对于堆栈指针都有一个正偏移量。 不需要负偏移量,因为它们只会指向未使用的堆栈空间。 当处理器支持堆栈指针相对寻址时,这可以简化对堆栈位置的访问。
许多处理器的指令允许使用相对于某些寄存器的仅正偏移量进行访问。 其中包括许多现代建筑,以及一些古老的建筑。 例如,ARM Thumb ABI 通过在单个 16 位指令字内编码的正偏移量提供与堆栈指针相关的访问。
如果堆栈向上增长,则相对于堆栈指针的所有有用偏移量都将为负,这不太直观且不太方便。 它也与寄存器相对寻址的其他应用不一致,例如访问结构体的字段。
Just a small addition to the other answers, which as far as I can see have not touched this point:
Having the stack grow downwards makes all addresses within the stack have a positive offset relative to the stack pointer. There's no need for negative offsets, as they would only point to unused stack space. This simplifies accessing stack locations when the processor supports stackpointer-relative addressing.
Many processors have instructions that allow accesses with a positive-only offset relative to some register. Those include many modern architectures, as well as some old ones. For example, the ARM Thumb ABI provides for stackpointer-relative accesses with a positive offset encoded within a single 16-bit instruction word.
If the stack grew upwards, all useful offsets relative to the stackpointer would be negative, which is less intuitive and less convenient. It also is at odds with other applications of register-relative addressing, for example for accessing fields of a struct.
向下增长的优点是在较旧的系统中,堆栈通常位于内存的顶部。 程序通常从底部开始填充内存,因此这种内存管理最大限度地减少了测量并将堆栈底部放置在合理位置的需要。
The advantage of growing down is in older systems the stack was typically at the top of memory. Programs typically filled memory starting from the bottom thus this sort of memory management minimized the need to measure and place the bottom of the stack somewhere sensible.
堆栈在 x86 上向下增长(由架构定义,弹出堆栈指针递增,入栈递减。)
Stack grows down on x86 (defined by the architecture, pop increments stack pointer, push decrements.)
在 MIPS 和许多现代 RISC 架构(如 PowerPC、RISC-V、SPARC...)没有
push
和pop
指令。 这些操作是通过手动调整堆栈指针然后相对于调整后的指针加载/存储值来显式完成的。 所有寄存器(除了零寄存器)都是通用的,因此理论上任何寄存器都可以是堆栈指针,并且堆栈可以在程序员想要的任何方向增长也就是说,在大多数体系结构上,堆栈通常会向下增长,可能是为了避免堆栈和程序数据或堆数据增长并相互冲突的情况。 还有 sh- 的回答提到的重要解决原因。 一些示例:MIPS ABI 向下增长并使用
$29
(又名$sp
)作为堆栈指针,RISC-V ABI 也向下增长并使用 x2 作为堆栈指针在 Intel 中8051 堆栈不断增长,可能是因为内存空间太小(原版为128字节),没有堆,不需要将堆栈放在顶部,这样就可以与底部增长的堆分开
您可以在 https://en.wikipedia.org/wiki 中找到有关各种架构中堆栈使用情况的更多信息/Calling_convention
另请参阅
In MIPS and many modern RISC architectures (like PowerPC, RISC-V, SPARC...) there are no
push
andpop
instructions. Those operations are explicitly done by manually adjusting the stack pointer then load/store the value relatively to the adjusted pointer. All registers (except the zero register) are general purpose so in theory any register can be a stack pointer, and the stack can grow in any direction the programmer wantsThat said, the stack typically grows down on most architectures, probably to avoid the case when the stack and program data or heap data grows up and clash to each other. There's also the great addressing reasons mentioned sh-'s answer. Some examples: MIPS ABIs grows downwards and use
$29
(A.K.A$sp
) as the stack pointer, RISC-V ABI also grows downwards and use x2 as the stack pointerIn Intel 8051 the stack grows up, probably because the memory space is so tiny (128 bytes in original version) that there's no heap and you don't need to put the stack on top so that it'll be separated from the heap growing from bottom
You can find more information about the stack usage in various architectures in https://en.wikipedia.org/wiki/Calling_convention
See also
在大多数系统上,堆栈会向下增长,我的文章位于 https://gist.github.com/cpq/ 8598782解释了为什么它会向下生长。 很简单:如何在固定的内存块中布局两个不断增长的内存块(堆和堆栈)? 最好的解决方案是将它们放在相对的两端,让它们朝着彼此生长。
On most systems, stack grows down, and my article at https://gist.github.com/cpq/8598782 explains WHY it grows down. It is simple: how to layout two growing memory blocks (heap and stack) in a fixed chunk of memory? The best solution is to put them on the opposite ends and let grow towards each other.
它向下增长,因为分配给程序的内存具有“永久数据”,即底部是程序本身的代码,然后是中间的堆。 您需要另一个固定点来引用堆栈,这样您就可以离开顶部。 这意味着堆栈向下增长,直到它可能与堆上的对象相邻。
It grows down because the memory allocated to the program has the "permanent data" i.e. code for the program itself at the bottom, then the heap in the middle. You need another fixed point from which to reference the stack, so that leaves you the top. This means the stack grows down, until it is potentially adjacent to objects on the heap.
这个宏应该在运行时检测到它而不需要 UB:
This macro should detect it at runtime without UB: