堆、栈的生长方向记录

作者:RednaxelaFX
链接:https://www.zhihu.com/question/36103513/answer/66101372
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

1. 堆没有方向之说,每个堆都是散落的
2. 堆和栈之间没有谁地址高之说,看操作系统实现
3. 数组取下标偏移总是往上涨的,和在堆还是栈没啥关系

 

简短回答:
进程地址空间的分布取决于操作系统,栈向什么方向增长取决于操作系统与CPU的组合。
不要把别的操作系统的实现方式套用到Windows上。

x86硬件直接支持的栈确实是“向下增长”的:push指令导致sp自减一个slot,pop指令导致sp自增一个slot。其它硬件有其它硬件的情况。

==========================================

栈的增长方向与栈帧布局

这个上下文里说的“栈”是函数调用栈,是以“栈帧”(stack frame)为单位的。
每一次函数调用会在栈上分配一个新的栈帧,在这次函数调用结束时释放其空间。
被调用函数(callee)的栈帧相对调用函数(caller)的栈帧的位置反映了栈的增长方向:如果被调用函数的栈帧比调用函数的在更低的地址,那么栈就是向下增长;反之则是向上增长。

而在一个栈帧内,局部变量是如何分布到栈帧里的(所谓栈帧布局,stack frame layout),这完全是编译器的自由。
至于数组元素与栈的增长方向:C与C++语言规范都规定了数组元素是分布在连续递增的地址上的。引用C语言规范的规定:
An array type describes a contiguously allocated nonempty set of objects with a particular member object type, called the element type.
A postfix expression followed by an expression in square brackets [] is a subscripted designation of an element of an array object. The definition of the subscript operator [] is that E1[E2] is identical to (*((E1)+(E2))). Because of the conversion rules that apply to the binary + operator, if E1 is an array object (equivalently, a pointer to the initial element of an array object) and E2 is an integer, E1[E2] designates the E2-th element of E1 (counting from zero).

其实double a0[4]这个声明告诉编译器的是:我需要在栈帧里分配一块连续的空间,大小为sizeof(double)*4,并且让a0引用该空间的起始位置(最低地址);
而不是说:我要根据栈的增长方向,先分配a0[0],然后分配a0[1],再分配a0[2],最后分配a0[3],于是如果栈是向下增长那a0[1]就应该比a0[0]在更低的地址——不是这样的。

所以在题主给的例子里,a0和a1这两个分配在栈帧里的数组到底哪个在高地址哪个在低地址,其实并不反映栈的增长方向,而只反映了编译器自己的决定。
C与C++语言的数组元素要分配在连续递增的地址上,也不反映栈的增长方向。

==========================================

以简化的Linux/x86模型为例

在简化的32位Linux/x86进程地址空间模型里,(主线程的)栈空间确实比堆空间的地址要高——它已经占据了用户态地址空间的最高可分配的区域,并且向下(向低地址)增长。借用Gustavo Duarte的Anatomy of a Program in Memory里的图:

不过要留意的是这个图是简化模型。举两个例子:
  • 虽然传统上Linux上的malloc实现会使用brk()/sbrk()来实现malloc()(这俩构成了上图中“Heap”所示的部分,这也是Linux自身所认为是heap的地方——用pmap看可以看到这里被标记为[heap]),但这并不是必须的——一个malloc()实现完全可以只用或基本上只用mmap()来实现malloc(),此时一般说的“Heap”(malloc-heap)就不一定在上图“Heap”(Linux heap)所示部分,而会在“Memory Mapping Segment”部分散布开来。不同版本的Linux在分配未指定起始地址的mmap()时用的顺序不一样,并不保证某种顺序。而且mmap()分配到的空间是有可能出现在低于主可执行程序映射进来的text Segment所在的位置。
  • Linux上多线程进程中,“线程”其实是一组共享虚拟地址空间的进程。只有主线程的栈是按照上面图示分布,其它线程的栈的位置其实是“随机”的——它们可以由pthread_create()调用mmap()来分配,也可以由程序自己调用mmap()之后把地址传给pthread_create()。既然是mmap()来的,其它线程的栈出现在Memory Mapping Segment的任意位置都不出奇,与用于实现malloc()用的mmap()空间很可能是交错出现的。

==========================================

Windows的进程地址空间

然而题主的实验是在Windows上而不是在Linux上做的,从截图看起来至少是Windows 7?
Windows的进程地址空间分布跟上面说的简化的Linux/x86模型颇不一样。
就算在没有ASLR的老Windows上也已经很不一样,有了ASLR之后就更加不一样了。

在Windows上不应该对栈和堆的相对位置做任何假设。
要想看个清楚Windows的进程地址空间长啥样,可以用Sysinternals出品的VMMap看看。该工具简介请见:VMMap - A Peek Inside Virtual Memory

 

posted @ 2020-02-14 22:52  wdliming  阅读(607)  评论(0编辑  收藏  举报