堆栈会向上还是向下生长?


88

我在c中有这段代码:

int q = 10;
int s = 5;
int a[3];

printf("Address of a: %d\n",    (int)a);
printf("Address of a[1]: %d\n", (int)&a[1]);
printf("Address of a[2]: %d\n", (int)&a[2]);
printf("Address of q: %d\n",    (int)&q);
printf("Address of s: %d\n",    (int)&s);

输出为:

Address of a: 2293584
Address of a[1]: 2293588
Address of a[2]: 2293592
Address of q: 2293612
Address of s: 2293608

因此,我看到从aa[2],内存地址每个增加4个字节。但是从qs,内存地址减少4个字节。

我想知道两件事:

  1. 堆栈是向上还是向下生长?(在这种情况下,对我来说都一样)
  2. a[2]q内存地址之间会发生什么?为什么那里有很大的内存差异?(20个字节)。

注意:这不是作业问题。我对堆栈的工作方式很好奇。谢谢你的帮助。


顺序是任意的。差距可能是存储中间结果,例如&q或&s-查看反汇编并亲自查看。
汤姆·莱斯

我同意,请阅读汇编代码。如果您要问这些问题,那么该学习阅读了。
Per Johansson,

Answers:


74

堆栈的行为(向上或向下)取决于应用程序二进制接口(ABI)以及调用堆栈(即激活记录)的组织方式。

程序在其整个生命周期中都必须与其他程序(例如OS)进行通信。ABI确定一个程序如何与另一个程序进行通信。

不同体系结构的堆栈可以以任何一种方式增长,但是对于体系结构而言,它将是一致的。请检查Wiki链接。但是,堆栈的增长是由该架构的ABI决定的。

例如,如果您使用MIPS ABI,则调用堆栈的定义如下。

让我们考虑函数“ fn1”调用“ fn2”。现在,由“ fn2”看到的堆栈框架如下:

direction of     |                                 |
  growth of      +---------------------------------+ 
   stack         | Parameters passed by fn1(caller)|
from higher addr.|                                 |
to lower addr.   | Direction of growth is opposite |
      |          |   to direction of stack growth  |
      |          +---------------------------------+ <-- SP on entry to fn2
      |          | Return address from fn2(callee) | 
      V          +---------------------------------+ 
                 | Callee saved registers being    | 
                 |   used in the callee function   | 
                 +---------------------------------+
                 | Local variables of fn2          |
                 |(Direction of growth of frame is |
                 | same as direction of growth of  |
                 |            stack)               |
                 +---------------------------------+ 
                 | Arguments to functions called   |
                 | by fn2                          |
                 +---------------------------------+ <- Current SP after stack 
                                                        frame is allocated

现在您可以看到堆栈向下生长。因此,如果将变量分配给函数的局部框架,则变量的地址实际上会向下增长。编译器可以决定内存分配的变量顺序。(在您的情况下,可以是'q'或's'是首先分配的堆栈内存。但是,通常,编译器按照变量声明的顺序进行堆栈内存分配)。

但是在数组的情况下,分配只有一个指针,需要分配的内存实际上将由单个指针指向。内存需要是连续的数组。因此,尽管堆栈向下增长,但对于阵列,堆栈却向上增长。


5
另外,如果要检查堆栈是向上还是向下生长。在main函数中声明一个局部变量。打印变量的地址。从main调用另一个函数。在函数中声明一个局部变量。打印其地址。根据打印的地址,我们可以说堆栈是向上还是向下。
2009年

谢谢Ganesh,我有一个小问题:在图中,您在第三个方框中绘制了您的意思是“在CALLER中使用了calleR保存的寄存器”,因为当f1调用f2时,我们必须存储f1地址(这是返回地址)对于f2)和f1(calleR)寄存器,而不是f2(callee)寄存器。对?
CSawy 2013年

44

这实际上是两个问题。一个是关于一个函数调用另一个函数(分配新框架时)堆栈增长的方式,另一个是关于如何在特定函数的框架中布置变量。

C标准均未指定两者,但答案略有不同:

  • 分配新帧时,堆栈会以哪种方式增长-如果函数f()调用函数g(),则f帧指针将大于还是小于g帧指针? 这可以采用任何一种方式-取决于特定的编译器和体系结构(查找“调用约定”),但是在给定平台内始终保持一致(有一些奇怪的异常,请参见注释)。向下比较常见。在x86,PowerPC,MIPS,SPARC,EE和Cell SPU中就是这种情况。
  • 函数的局部变量如何在其堆栈框架内布置?这是不确定的,也是完全不可预测的;编译器可以自由安排其局部变量,但它希望获得最有效的结果。

7
“在给定平台内始终保持一致”-不保证。我见过一个没有虚拟内存的平台,其中的堆栈是动态扩展的。实际上已分配了新的堆栈块,这意味着您将“向下”放置一个堆栈块一段时间,然后突然“侧身”进入另一个块。“边路”可能意味着更大或更小的地址,这完全取决于抽奖的运气。
史蒂夫·杰索普

2
关于第2项的其他详细信息-编译器可以决定一个变量永远不需要在内存中(在变量的生命周期内将其保存在寄存器中),和/或两个或多个变量的生命周期是否满足要求,如果重叠,编译器可能会决定对多个变量使用同一内存。
迈克尔·伯

2
我认为S / 390(IBM zSeries)具有一个ABI,其中链接了呼叫帧而不是在堆栈上增长。
短暂的

2
在S / 390上更正。调用是“ BALR”,分支和链接寄存器。返回值被放入寄存器,而不是被压入堆栈。返回函数是该寄存器内容的分支。随着堆栈的深入,将在堆中分配空间并将它们链接在一起。这是MVS等效于“ / bin / true”的地方的名称:“ IEFBR14”。第一个版本只有一条指令:“ BR 14”,它跳转到包含返回地址的寄存器14的内容。
2009年1

1
而且,PIC处理器上的某些编译器会进行整个程序分析,并为每个函数的自动变量分配固定的位置。实际的堆栈很小,无法通过软件访问;它仅用于寄信人地址。
09年

13

堆栈增长的方向是特定于体系结构的。也就是说,我的理解是只有很少的硬件体系结构具有不断增长的堆栈。

堆栈增长的方向与单个对象的布局无关。因此,尽管堆栈可能变小,但数组不会(即&array [n]始终为<&array [n + 1]);


4

该标准中没有任何内容规定如何在堆栈上组织一切。实际上,您可以构建一个合格的编译器,该编译器根本不将数组元素存储在堆栈上的连续元素上,只要它具有能够正确地对数组元素进行算术的智能(例如,它知道1为距a [0] 1K,可以对此进行调整)。

得到不同结果的原因是,尽管堆栈可能会变小以向其中添加“对象”,但数组是单个“对象”,并且它可能具有相反顺序的升序数组元素。但是,依靠这种行为并不安全,因为方向可能会发生变化,并且由于各种原因,变量可能会被交换,包括但不限于:

  • 优化。
  • 对准。
  • 人的奇思妙想编译器的堆栈管理部分。

请参阅此处,以获得我在堆栈方向上的出色论文:-)

在回答您的特定问题时:

  1. 堆栈是向上还是向下生长?
    根本不重要(就标准而言),但是,正如您所要求的,它可以在内存中增长下降,具体取决于实现方式。
  2. a [2]和q存储器地址之间会发生什么?为什么那里有很大的内存差异?(20个字节)?
    根本没有关系(就标准而言)。请参阅上面的可能原因。

我看到您链接到大多数CPU体系结构都采用“向下增长”方式,您知道这样做有什么好处吗?
Baiyan Huang

不知道,真的。这可能是有人想代码从0向上堆栈所以应从HIGHMEM向下走,以尽量减少交叉的可能性。但是某些CPU专门在非零位置开始运行代码,因此可能并非如此。与大多数事情一样,也许这样做是因为它是人们认为这样做的第一种方法:-)
paxdiablo 2011年

@lzprgmr:以升序执行某些类型的堆分配有一些轻微的优势,并且从历史上看,堆栈和堆位于公共寻址空间的相对两端是很常见的。只要静态+堆+堆栈的组合使用不超过可用内存,就不必担心程序使用了多少堆栈内存。
2014年

3

在x86上,堆栈帧的内存“分配”仅包括从堆栈指针中减去必要的字节数(我相信其他体系结构是相似的)。从这个意义上讲,我猜堆栈会“向下”增长,因为当您更深入地调用堆栈时,地址会逐渐变小(但是我总是设想内存从左上角的0开始,随着移动而变大右边并结束,所以在我的心理图像中,堆栈长大了……)。声明变量的顺序可能与它们的地址没有关系-我相信该标准允许编译器对它们进行重新排序,只要它不会引起副作用(如果我错了,请纠正我) 。他们'

数组周围的间隙可能是某种填充,但这对我来说很神秘。


1
实际上,我知道编译器可以对其重新排序,因为完全不分配它们也是免费的。它可以将它们放入寄存器,而不会使用任何堆栈空间。
rmeador

如果您引用它们的地址,则不能将它们放入寄存器。
佛罗里达州

好点,没有考虑。但它仍然足以证明编译器可以对其进行重新排序,因为我们知道它至少可以在某些时间进行:)
rmeador

1

首先,它在内存中的8个字节的未使用空间(不是12,记住堆栈向下增长,所以未分配的空间是从604到597)。为什么?因为每种数据类型都会从其大小可除的地址开始占用内存空间。在我们的例子中,由3个整数组成的数组占用12个字节的内存空间,而604不能被12整除。因此,它留下空白直到遇到遇到被12整除的内存地址,即596。

因此,分配给数组的内存空间从596到584。但是,由于数组分配是连续的,所以数组的第一个元素从584地址而不是从596开始。


1

编译器可以自由地在本地堆栈帧上的任何位置分配本地(自动)变量,您不能仅凭此可靠地推断堆栈的增长方向。您可以通过比较嵌套堆栈框架的地址(即,将函数的堆栈框架内的局部变量的地址与其被调用方的地址进行比较)来推断堆栈的增长方向:

#include <stdio.h>
int f(int *x)
{
  int a;
  return x == NULL ? f(&a) : &a - x;
}

int main(void)
{
  printf("stack grows %s!\n", f(NULL) < 0 ? "down" : "up");
  return 0;
}

5
我很确定,减去指向不同堆栈对象的指针是未定义的行为-不属于同一对象的指针是不可比较的。显然,尽管它不会在任何“正常”架构上崩溃。
史蒂夫·杰索普

@SteveJessop有什么办法可以解决此问题,以编程方式获得堆栈的方向?
xxks-kkk

@ xxks-kkk:原则上不,因为不需要C实现具有“堆栈方向”。例如,有一个调用约定,在该约定中先分配了一个堆栈块,然后使用一些伪随机内部存储器分配例程在其中进行跳转,这不会违反该标准。实际上,它确实按照matja的描述工作。
史蒂夫·杰索普

0

我认为这不是确定性的。数组似乎“增长”,因为该内存应连续分配。但是,由于q和s根本不相关,因此编译器只是将它们分别放在堆栈中的任意空闲内存位置,可能是最适合整数大小的位置。

在a [2]和q之间发生的事情是q的位置周围的空间不够大(即,不大于12个字节)以分配3个整数数组。


如果是这样,为什么q,s,a没有连续记忆?(例如:q的地址:2293612 s的地址:2293608 a的地址:2293604)

我看到s与

因为s和a没有一起分配-数组中唯一的指针必须是连续的。可以在任何地方分配其他内存。
javanix

0

我的堆栈似乎向编号较小的地址扩展。

在另一台计算机上,甚至在我自己的计算机上,如果使用不同的编译器调用,它可能会有所不同。...或编译器muigt选择根本不使用栈(内联所有内容(如果我不使用函数和变量的地址,则为函数和变量))。

$ cat stack.c
#include <stdio.h>

int stack(int x) {
  printf("level %d: x is at %p\n", x, (void*)&x);
  if (x == 0) return 0;
  return stack(x - 1);
}

int main(void) {
  stack(4);
  return 0;
}
$ / usr / bin / gcc -Wall -Wextra -std = c89 -pedantic stack.c
$ ./每年
级别4:x为0x7fff7781190c
级别3:x为0x7fff778118ec
级别2:x为0x7fff778118cc
级别1:x为0x7fff778118ac
级别0:x位于0x7fff7781188c

0

堆栈变小(在x86上)。但是,当函数加载时,堆栈被分配在一个块中,并且您不能保证这些项目在堆栈中的顺序。

在这种情况下,它为堆栈上的两个整数和一个三个整数数组分配了空间。它还在数组之后分配了额外的12个字节,因此如下所示:

a [12字节]
padding(?)[12字节]
s [4字节]
q [4字节]

无论出于何种原因,编译器都决定需要为此功能分配32个字节,甚至可能更多。作为C程序员,这对您来说是不透明的,您不知道为什么。

如果您想知道为什么,请将代码编译为汇编语言,我相信它是gcc上的-S和MS的C编译器上的/ S。如果查看该函数的打开说明,则会看到保存了旧的堆栈指针,然后从中减去了32(或其他东西!)。从那里,您可以看到代码如何访问该32字节的内存块,并了解编译器在做什么。在函数末尾,您可以看到正在还原的堆栈指针。


0

这取决于您的操作系统和编译器。


不知道为什么我的答案被否决了。它确实取决于您的操作系统和编译器。在某些系统上,堆栈向下生长,但在其他系统上,则向上生长。在某些系统上,没有真正的下推框架堆栈,而是使用内存或寄存器集的保留区域进行模拟的。
David R Tribble

3
可能是因为单句断言不是好的答案。
Lightness Races in Orbit

0

堆栈确实长大了。因此,f(g(h())),为h分配的堆栈将从较低的地址开始,然后从g开始,而g的堆栈将比f的较低。但是堆栈中的变量必须遵循C规范,

http://c0x.coding-guidelines.com/6.5.8.html

1206如果所指向的对象是同一聚合对象的成员,则指向稍后声明的结构成员的指针大于指向早于结构中声明的成员的指针,指向具有较大下标值的数组元素的指针大于指向相同元素的指针下标值较低的数组。

&a [0] <&a [1],必须始终为true,无论如何分配“ a”


在大多数机器上,堆栈向下生长-向上生长的除外。
乔纳森·莱夫勒

0

向下增长,这是由于涉及内存中的数据集时使用了小字节序字节顺序标准。

一种看待它的方法是,如果从顶部开始从0开始,从底部开始从max开始查看内存,则堆栈确实会向上增长。

堆栈向下增长的原因是能够从堆栈或基本指针的角度取消引用。

请记住,任何类型的取消引用都会从最低地址到最高地址增加。由于堆栈向下增长(从最高到最低地址),因此您可以像对待动态内存一样对待堆栈。

这就是为什么这么多的编程和脚本语言使用基于堆栈的虚拟机而不是基于寄存器的原因之一。


The reason for the stack growing downward is to be able to dereference from the perspective of the stack or base pointer.很好的推理
user3405291

0

这取决于架构。要检查您自己的系统,请使用GeeksForGeeks中的以下代码:

// C program to check whether stack grows 
// downward or upward. 
#include<stdio.h> 

void fun(int *main_local_addr) 
{ 
    int fun_local; 
    if (main_local_addr < &fun_local) 
        printf("Stack grows upward\n"); 
    else
        printf("Stack grows downward\n"); 
} 

int main() 
{ 
    // fun's local variable 
    int main_local; 

    fun(&main_local); 
    return 0; 
} 
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.