C语言中栈变量是反向存储的吗?

6

我正在尝试理解C语言在栈上分配内存的方式。我一直认为栈上的变量可以像结构体成员变量一样表示,它们在栈内占用连续的字节块。为了帮助说明这个问题,我在某处找到了一个示例程序,它重现了这种现象。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void function(int  *i) {
    int *_prev_int =  (int *) ((long unsigned int) i -  sizeof(int))  ;
    printf("%d\n", *_prev_int );    
}

void main(void) 
{
    int x = 152;
    int y = 234;
    function(&y);
}

你看到我在做什么了吗?假设 sizeof(int) 是4:我查看传递指针的4个字节,因为这将读取调用者栈中 int y 之前的4个字节。

它没有打印出152。奇怪的是,当我查看下面的4个字节时:

int *_prev_int =  (int *) ((long unsigned int) i +  sizeof(int))  ;

现在它已经运行,打印出来的是调用栈中x变量的内容。为什么x的地址比y低?堆栈变量是倒序存储的吗?

2
我认为这是实现定义的/或未指定的。请查看此答案https://dev59.com/63E95IYBdhLWcg3wPbZ7#4105123 - Grijesh Chauhan
3
这完全取决于不同的平台,但许多主流平台实际上会将堆从代码/数据段向上增长,并将栈从(可用)内存顶部向下增长。但是,在编写C代码时,您绝不能依赖于此...请注意不要改变原意。 - DevSolar
1
“int* prev = i - 1;” 这么简单的语句也能达到同样的效果吗? - mch
1
C语言不会分配任何东西。它只是一个规范。它对你尝试检查的内容绝对没有任何说明。这意味着你不能以任何方式依赖实验结果。或者赋予它们任何含义。 - n. m.
1
要了解为什么你的实验毫无意义,请查看这里 - n. m.
显示剩余9条评论
3个回答

11

堆栈的组织完全是未指定的,是实现特定的。实际上,它很大程度上取决于编译器(甚至是其版本)和优化标志。

有些变量甚至不在堆栈上(例如,因为它们只保留在某些寄存器中,或者因为编译器优化了它们 - 例如内联、常量折叠等等)。

顺便说一句,您可能会有一些假设的C实现,它不使用任何堆栈(即使我不能命名这样的实现)。

要了解更多关于堆栈的知识:

请阅读有关 调用堆栈尾调用线程continuations 的维基页面。
熟悉您计算机的 架构指令集(例如 x86)和 ABI,然后...
要求编译器显示汇编代码和/或一些中间编译器表示。如果使用 GCC,请使用 gcc -S -fverbose-asm 编译一些简单的代码(在编译 foo.c 时获取汇编代码 foo.s),并尝试几个优化级别(至少 -O0, -O1, -O2 ...)。还可以尝试 -fdump-tree-all 选项(它会转储数百个文件,显示编译器针对源代码的一些内部表示)。请注意,GCC 还提供了 返回地址内置函数
阅读 Appel 的旧论文,垃圾回收可以比堆栈分配更快,并了解 垃圾回收 技术(因为它们经常需要检查并可能更改调用堆栈帧内的某些指针)。要了解有关 GC 的更多信息,请阅读 GC 手册

遗憾的是,我不了解低级语言(如C、D、Rust、C++、Go等),这些语言在语言层面上无法访问调用堆栈。这就是为什么编写C语言的垃圾收集器很困难(因为GC需要扫描调用堆栈指针)...但可以参考Boehm's conservative GC提供的非常实用和实际的解决方案。


4
据我所知,C99标准没有规范地提到任何栈。 - Basile Starynkevitch
1
@GrijeshChauhan:不需要引用标准,因为它在标准中是未指定的。这就是为什么你不应该依赖它的行为。 - DevSolar
关于“假设的C实现”- 我记得有一些针对gcc的调整,可以编译最低端的AVR处理器,这些处理器没有RAM(或堆栈指针),只有32x8位寄存器。 - JimmyB
1
@GrijeshChauhan: 既然你有一个文本文档,那就搜索“stack”并查看它为什么没有给出单一匹配。参数和局部变量的存储位置未被标准定义--即未指明的行为。也许你的平台或编译器记录了它的操作方式,这意味着它是实现定义的 - DevSolar
2
@grijeshchauhan:实际上,我认为添加这样的信息会带来更多的伤害而不是好处,因为太多的程序员不理解依赖于未指定/实现定义行为的影响,并且愉快地使用任何“对我有效”的东西,然后想知道为什么下一个编译器或操作系统更新会破坏他们的代码。 - DevSolar
显示剩余4条评论

3
现在几乎所有的处理器架构都支持堆栈操作指令(例如ARM中的LDM、STM指令)。编译器在这些指令的帮助下实现堆栈操作。通常情况下,当数据被推入堆栈时,堆栈指针会递减(向下增长),而数据从堆栈中弹出时,堆栈指针会递增。
因此,堆栈的实现方式取决于处理器架构和编译器。

1
这非常取决于编程语言。Go分配堆来管理其“栈”变量。换句话说,在Go程序中,“栈”指针增加的可能性更大。 - Alexis Wilke
@AlexisWilke,没错,但问题是关于'C'的。 - Vagish
不错的观点,尽管 C 的实现“可以”选择像 Go 一样去做。 - Alexis Wilke

1
取决于编译器和平台。只要程序(在这种情况下是编译器将其翻译成汇编语言,即机器代码)保持一致,并且平台支持它,就可以用多种方式完成同样的事情(好的编译器会尝试优化汇编以获得每个平台的“最大值”)。
深入了解C语言背后的技术,了解编译程序时发生的事情以及为什么会发生,一个非常好的资源是Dennis Yurichev的免费书籍《逆向工程入门(理解汇编语言)》,最新版本可在他的网站上找到

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接