答案 0 :(得分:9)
使用x86,将数据“推送”到堆栈的指令也会修改堆栈指针(在这种情况下为%esp
)以标记新的堆栈顶部。 “弹出”数据的指令以相反的方向修改堆栈指针。
在没有特殊推送和弹出指令的机器上,程序必须首先修改堆栈指针,然后将数据存储到堆栈。
通常,为堆栈保留大面积区域。堆栈指针仅标记当前正在使用的部分。程序可以根据需要自由地上下移动堆栈指针。
为堆栈保留的区域可能取决于操作系统和/或开发人员工具。例如,在使用Apple开发人员工具的macOS上,默认堆栈大小为8兆字节,可以通过“-stack_size size ”切换到链接器(ld
命令)进行更改。 (这是针对主堆栈的。使用多个线程的程序为每个创建的线程都有一个额外的堆栈。这些堆栈的堆栈大小是单独设置的。)
虽然为堆栈保留了大面积的虚拟地址空间,但是一旦程序启动,操作系统可能不会将其全部映射到物理内存。操作系统可能只映射其中的一部分,然后在堆栈扩展到区域时映射更多部分。
通常,堆栈之外的虚拟地址空间的某些部分保持未映射,因此尝试访问它将导致异常。此区域中的地址空间页面称为保护页面。因此,如果程序使堆栈超出保留区域并尝试将值写入未映射的防护页面,则会发生异常,系统将报告堆栈溢出。
没有什么能阻止程序写入为堆栈保留的区域,但稍微超出堆栈指针。这将是一个错误,但通常不会被硬件检测到。此外,执行此操作的程序可能会正常运行一段时间;它可以将数据存储到该区域并按预期加载回来。但是,您的过程中还会发生一些您通常不知道的事情。例如,可以将信号传递给您的过程。发生这种情况时,系统会中断程序的常规处理,将新数据压入堆栈,并调用信号处理程序例程。例程返回时,数据将从堆栈中删除,程序将恢复正常执行。但是,如果您的程序已经存储了超出堆栈指针的数据,那么该数据现在已经消失,因为它被信号处理程序的数据覆盖了。因此,存储超出堆栈指针的数据的程序似乎在大多数时间都可以工作,但在信号到达错误时刻的极少数情况下会失败。
(在某些系统上,堆栈的安全区域实际上是超出堆栈指针中地址的固定距离,而不是指向该地址。这个额外的安全空间可能被称为“红区”。)
答案 1 :(得分:2)
大多数细节见@ Eric的答案。
一些操作系统不仅懒得将整个保留堆栈区域实际映射到物理页面,有些甚至根本没有逻辑映射它。例如在Linux上,/proc/self/maps
中堆栈映射的大小小于ulimit -s
值。但触摸该区域的内存将导致内核扩展映射(达到该大小限制),即使它远远低于当前映射的结尾。
这与通常的延迟映射是分开的,其中新分配的mmap(MAP_ANONYMOUS)
区域的所有页面都是写时复制映射到相同的物理页面(全部为零)。因此,读取新页面可以为您提供TLB未命中(为该虚拟地址遍历页表)和L1D缓存命中(因为所有仍然写入的页面的物理地址相同) 1
在其他操作系统(如我认为的Windows)上,你不能在一次跳跃中走得太远;如果访问位于当前映射的最低地址页面的几页内,则内核的页面错误处理程序将仅为您映射新的堆栈内存。 (它也可能会检查ESP / RSP是否低于故障地址)。如果这些检查中的任何一个失败,则页面错误会导致程序出现异常,而不是通过静默映射该页面并重新运行加载或存储指令来处理。
这意味着为堆栈上的大型数组分配空间必须在每个页面左右探测堆栈,在需要它的操作系统上。微软的Visual Studio编译器文档中有一些关于细节的信息:the /Gs
option默认为/Gs4096
:当大量增加堆栈时,每4k至少探测一次,或者对于可变大小的数组,可能那么大。
“探测”并不特别,只是加载或存储到堆栈地址,如果页面尚未分配,则触发页面错误,因此堆栈一次增长一页而不是错误。
<强>脚注强>:
malloc
和calloc
使用mmap
,而calloc
知道mmap
将其归零,因此不会重做归零当它从内核获得新的内存时。
但是C ++ std::vector
太愚蠢了(在gcc和clang中,有libc++
或libstdc++
),并且即使对于{{1}的大型分配也会弄脏所有新内存本身最终调用new
。 C ++的可替换 - mmap
意味着编译器/库只能用new
或链接时优化来优化它,但理论上它是可能的。因为在实践中它不会发生,所以使用自定义分配器或避免-fwhole-program
如果此行为有用(例如,只写一部分的大型稀疏分配)。