关于堆栈增长和寻址的困惑
Confusion about stack growth and addressing
我正在尝试更好地理解堆栈中的项目以及它们的寻址方式。我找到的文章 here 似乎表明,当初始化 MIPS 堆栈时,会分配固定数量的内存,并且堆栈会向下增长到堆栈限制,这似乎是更小的地址。我会假设基于这个逻辑当遍历0x0000时会发生堆栈溢出?
我知道 MIPS 是大端,但这会改变堆栈的增长方式吗?我写了我认为可以在 x86_64 机器上快速观察这一点的方法,但堆栈似乎在增长,正如我最初假设的那样。
#include <iostream>
#include <vector>
int main() {
std::vector<int*> v;
for( int i = 0; i < 10; i++ ) {
v.push_back(new int);
std::cout << v.back() << std::endl;
}
}
我也对并非所有内存地址都不是连续的这一事实感到困惑,这让我觉得我做了一些愚蠢的事情。有人可以澄清一下吗?
x86 机器上的栈也是向下增长的。字节顺序与堆栈增长的方向无关。
一台机器的堆栈与std::vector<>
完全没有关系。此外,new int
分配堆内存,因此它绝对不会告诉您有关堆栈的任何信息。
为了查看堆栈的增长方向,您需要执行以下操作:
recursive( 5 );
void recursive( int n )
{
if( n == 0 )
return;
int a;
printf( "%p\n", &a );
recursive( n - 1 );
}
(请注意,如果您的编译器足够聪明,可以优化尾递归,那么您需要告诉它不要优化它,否则观察结果将全部错误。)
本质上,您在编程中使用了 3 种类型的内存:静态、dynamic/heap 和堆栈。
静态内存由编译器预先分配,由程序中静态声明的常量和变量组成。
Heap是你可以自由分配和释放的内存
堆栈是为函数中声明的所有局部变量分配的内存。这很重要,因为每次调用该函数时,都会为其变量分配新的内存。因此,每次调用函数都将确保它拥有自己唯一的变量副本。每次你从函数 return 中释放内存。
只要遵循上述规则,堆栈如何管理就完全没有关系。然而,让程序内存分配在较低地址 space 并向上增长,而堆栈从顶部内存 space 开始并向下增长是很方便的。大多数系统都实现了这个方案。
一般有一个栈指针register/variable指向当前栈地址。当一个函数被调用时,它会将这个地址减少它的变量所需的字节数。当它调用下一个函数时,这个新函数将从调用者已经减少的新指针开始。当函数 returns 它恢复它开始的指针。
可能有不同的方案,但据我所知,mips 和 i86 遵循这一方案。
而且程序中本质上只有一个虚拟内存space。这取决于操作系统 and/or 编译器如何使用它。编译器将在逻辑区域中拆分内存供自己使用,并希望根据平台文档中定义的调用约定来处理它们。
因此,在我们的示例中,v
和 i
分配在函数堆栈上。 cout
是静态的。每个 new int
在堆中分配 space。 v
不是一个简单的变量,而是一个包含管理列表所需字段的结构。所有这些内部结构都需要 space。因此,每个 push_back
都会以某种方式修改这些字段以指向分配的 'int'。 push_back() 和 back() 是函数调用,并为内部变量分配自己的堆栈,以免干扰 top 函数。
我正在尝试更好地理解堆栈中的项目以及它们的寻址方式。我找到的文章 here 似乎表明,当初始化 MIPS 堆栈时,会分配固定数量的内存,并且堆栈会向下增长到堆栈限制,这似乎是更小的地址。我会假设基于这个逻辑当遍历0x0000时会发生堆栈溢出?
我知道 MIPS 是大端,但这会改变堆栈的增长方式吗?我写了我认为可以在 x86_64 机器上快速观察这一点的方法,但堆栈似乎在增长,正如我最初假设的那样。
#include <iostream>
#include <vector>
int main() {
std::vector<int*> v;
for( int i = 0; i < 10; i++ ) {
v.push_back(new int);
std::cout << v.back() << std::endl;
}
}
我也对并非所有内存地址都不是连续的这一事实感到困惑,这让我觉得我做了一些愚蠢的事情。有人可以澄清一下吗?
x86 机器上的栈也是向下增长的。字节顺序与堆栈增长的方向无关。
一台机器的堆栈与std::vector<>
完全没有关系。此外,new int
分配堆内存,因此它绝对不会告诉您有关堆栈的任何信息。
为了查看堆栈的增长方向,您需要执行以下操作:
recursive( 5 );
void recursive( int n )
{
if( n == 0 )
return;
int a;
printf( "%p\n", &a );
recursive( n - 1 );
}
(请注意,如果您的编译器足够聪明,可以优化尾递归,那么您需要告诉它不要优化它,否则观察结果将全部错误。)
本质上,您在编程中使用了 3 种类型的内存:静态、dynamic/heap 和堆栈。
静态内存由编译器预先分配,由程序中静态声明的常量和变量组成。
Heap是你可以自由分配和释放的内存
堆栈是为函数中声明的所有局部变量分配的内存。这很重要,因为每次调用该函数时,都会为其变量分配新的内存。因此,每次调用函数都将确保它拥有自己唯一的变量副本。每次你从函数 return 中释放内存。
只要遵循上述规则,堆栈如何管理就完全没有关系。然而,让程序内存分配在较低地址 space 并向上增长,而堆栈从顶部内存 space 开始并向下增长是很方便的。大多数系统都实现了这个方案。
一般有一个栈指针register/variable指向当前栈地址。当一个函数被调用时,它会将这个地址减少它的变量所需的字节数。当它调用下一个函数时,这个新函数将从调用者已经减少的新指针开始。当函数 returns 它恢复它开始的指针。
可能有不同的方案,但据我所知,mips 和 i86 遵循这一方案。
而且程序中本质上只有一个虚拟内存space。这取决于操作系统 and/or 编译器如何使用它。编译器将在逻辑区域中拆分内存供自己使用,并希望根据平台文档中定义的调用约定来处理它们。
因此,在我们的示例中,v
和 i
分配在函数堆栈上。 cout
是静态的。每个 new int
在堆中分配 space。 v
不是一个简单的变量,而是一个包含管理列表所需字段的结构。所有这些内部结构都需要 space。因此,每个 push_back
都会以某种方式修改这些字段以指向分配的 'int'。 push_back() 和 back() 是函数调用,并为内部变量分配自己的堆栈,以免干扰 top 函数。