我输出了一个更长的答案,但是它很乱,所以这里有一个较短的答案......
当进程启动时,它需要一个堆栈,以便在调用函数时存储临时值(即自动分配)或堆栈帧.这个堆栈的内存必须来自某个地方.
因此,OS所做的是在堆栈的虚拟内存中创建映射,并将堆栈指针分配给该块的高地址.在预定义堆栈中,堆栈指针在解除引用之前递减,初始堆栈指针实际上是超过映射空间中最后一个地址的地址.
当进程尝试在堆栈上放置某些东西时,它会导致访问此内存区域,该区域没有映射到它的任何物理RAM.这会导致页面错误,从而导致操作系统将最少使用(或接近它)的RAM页面踢入交换驱动器或页面文件,并将物理RAM页面重新分配给正在访问的堆栈页面.现在有物理RAM,操作系统返回并继续进程,将推送的数据放入堆栈内存.
那么如果你把所有东西从堆栈中弹出然后尝试再次弹出会发生什么呢?最后一个pop,其中堆栈指针处于其初始值,导致访问未映射到堆栈的虚拟内存地址.这会导致分段错误,这意味着该进程尝试访问它从未分配的内存.操作系统通过终止进程并尽可能清理它来做出响应.
为什么不将页面映射到堆栈的末尾?因为这会导致读取未初始化的RAM,其中包含过去使用该物理RAM页面的内容.这根本不可能产生一个功能正常的程序(更不用说这是一个巨大的安全风险),所以最好还是杀死程序.
这将是堆栈本身实现的一部分。如果您在(例如)C 中实现堆栈,则可以存储堆栈指针和当前元素数。或者堆栈指针以及堆栈底部,例如:
typedef struct {
int *sp_empty;
int *sp;
int *sp_full;
} tIntStack;
tIntStack stk;
// Initialise 20-element stack.
stk.sp = stk.sp_empty = malloc (sizeof(int) * 20);
stk.sp_full = &(stack[20]);
// Push a value x, detecting overflow.
if (stk.sp == stk.sp_full) { error here}
*(stk.sp) = x;
stk.sp++;
// Pop a value x, detecting underflow.
if (stk.sp == stk.sp_empty) { error here}
stk.sp--;
x = *(stk.sp);
Run Code Online (Sandbox Code Playgroud)
如果您正在谈论 CPU 堆栈(返回地址等),您可能会由于崩溃而检测到堆栈下溢。糟糕。
举个例子,在过去,当处理器仅限于 64K 地址空间时,CPU 通常会测试内存,直到发现第一个字节读取的内容与刚刚写入的内容不同(在启动过程中)。这是超出实际物理内存的第一个字节,因此他们将 SP 设置为低于该字节。当然,一些(少数)机器有64K 物理 RAM,因此只需将 SP 设置为地址空间的顶部即可。
如今,在大地址空间、虚拟内存、多任务操作系统中,这个过程稍微复杂一些,但仍然可以归结为(在大多数情况下):
到那时,就内核而言,您可能只能靠自己了。它的责任在您的代码开始运行(除了任务切换和按请求提供服务之外)时停止,但这与您的堆栈无关。如果您的错误代码溢出或下溢堆栈,那就是您的问题。
内核可能会在任务切换时检查你的 SP,看看你是否做错了什么,但这绝不是保证的。它还可以使用硬件内存保护来检测下溢(如果堆栈位于分配的地址空间的顶部)。但同样,这完全取决于您使用的内核。