我正在准备一些 C 语言培训材料,我希望我的示例适合典型的堆栈模型。
C 堆栈在 Linux、Windows、Mac OSX(PPC 和 x86)、Solaris 和最新的 Unix 中的发展方向是什么?
我正在准备一些 C 语言培训材料,我希望我的示例适合典型的堆栈模型。
C 堆栈在 Linux、Windows、Mac OSX(PPC 和 x86)、Solaris 和最新的 Unix 中的发展方向是什么?
堆栈增长通常不取决于操作系统本身,而是取决于运行它的处理器。例如,Solaris 在 x86 和 SPARC 上运行。Mac OSX(如您所述)在 PPC 和 x86 上运行。Linux 可以在任何东西上运行,从我在工作中的大喇叭 System z 到一个微不足道的小手表。
如果 CPU 提供任何类型的选择,操作系统使用的 ABI / 调用约定指定如果您希望您的代码调用其他所有人的代码,您需要做出哪种选择。
处理器及其方向是:
显示我的年龄,1802 是用于控制早期航天飞机的芯片(我怀疑,根据它的处理能力来感应门是否打开 :-) 和我的第二台计算机COMX-35(跟随我的ZX80)。
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,2 个用于 DMA 和内存中断)。啊,营销对现实的胜利:-)
System z 实际上非常相似,使用它的 R14 和 R15 寄存器进行调用/返回。
在 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);
}
}
向下增长的优势在于,在旧系统中,堆栈通常位于内存的顶部。程序通常从底部开始填充内存,因此这种内存管理最大限度地减少了测量堆栈底部并将其放置在合理位置的需要。
堆栈在 x86 上向下增长(由架构定义,pop 递增堆栈指针,push 递减。)
只是对其他答案的一个小补充,据我所知,这还没有触及这一点:
让堆栈向下增长使堆栈内的所有地址都具有相对于堆栈指针的正偏移量。不需要负偏移,因为它们只会指向未使用的堆栈空间。当处理器支持堆栈指针相对寻址时,这简化了访问堆栈位置。
许多处理器的指令允许使用相对于某个寄存器的仅正偏移量进行访问。其中包括许多现代建筑,以及一些古老的建筑。例如,ARM Thumb ABI 为堆栈指针相关访问提供了在单个 16 位指令字中编码的正偏移量。
如果堆栈向上增长,所有相对于堆栈指针的有用偏移量都将为负数,这不太直观且不太方便。它也与寄存器相对寻址的其他应用程序不一致,例如访问结构的字段。
在 MIPS 和许多现代RISC 架构(如 PowerPC、RISC-V、SPARC...)中没有push
和pop
指令。这些操作是通过手动调整堆栈指针显式完成的,然后相对于调整后的指针加载/存储值。所有寄存器(除了零寄存器)都是通用的,所以理论上任何寄存器都可以是堆栈指针,堆栈可以向程序员想要的任何方向增长
也就是说,堆栈通常在大多数体系结构上增长,可能是为了避免堆栈和程序数据或堆数据增长并相互冲突的情况。还有提到sh-'s answer的重要解决原因。一些例子:MIPS ABI 向下增长并使用$29
(AKA $sp
) 作为堆栈指针,RISC-V ABI 也向下增长并使用 x2 作为堆栈指针
在 Intel 8051 中,堆栈增长,可能是因为内存空间非常小(原始版本为 128 字节)以至于没有堆,并且您不需要将堆栈放在顶部,以便将其与堆增长分开从底部
您可以在https://en.wikipedia.org/wiki/Calling_convention中找到有关各种架构中堆栈使用的更多信息
也可以看看
在大多数系统上,堆栈会变小,我在https://gist.github.com/cpq/8598782上的文章解释了为什么它会变小。很简单:如何在一块固定的内存中布局两个不断增长的内存块(堆和栈)?最好的解决方案是把它们放在相反的两端,让它们相互生长。
它会增长,因为分配给程序的内存有“永久数据”,即程序本身的代码在底部,然后是堆在中间。您需要另一个固定点来引用堆栈,这样您就可以在顶部。这意味着堆栈会向下增长,直到它可能与堆上的对象相邻。
这个宏应该在没有 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;
}