系统在提交栈空间时会故意多提交一个页面,称这个页面为栈保护页面(Stack Guard Page)。栈保护页面具有特殊的PAGE_GUARD属性,当具有如此属性的内存页被访问时,CPU会产生页错误并开始执行系统的内存管理函数,当内存管理函数检测到PAGE_GUARD属性后,会清除对应页面的PAGE_GUARD属性,然后调用一个名为MiCheckForUserStackOverflow的系统函数,这个函数会从当前线程的TEB中读取用户态栈的基本信息并检查导致异常的地址,如果导致异常的被访问地址不属于栈空间范围,则返回STATUS_GUARD_PAGE_VIOLATION,否则MiCheckForUserStackOverflow函数会计算栈中是否还有足够的剩余空间可以创建一个新的栈保护页面。如果有,则调用ZwAllocateVirtualMemory从保留的空间中在提交一个具有PAGE_GUARD属性的内存页。新的栈保护页与原来的紧邻,经过这样的操作后,栈的保护页向低地址方向平移了一位,栈的可用空间增大了一个页面的大小,这便是所谓的栈空间自动增长。
栈溢出是指当提交的栈空间再被用完,栈保护页又被访问时,系统便会重复以上过程,直到当栈保护页距离保留空间的最后一个页面只剩一个页面的空间时,MiCheckForUserStackOverflow函数会提交倒数第二个页面,但不再设置PAGE_GUARD属性,因为最后一个页面永远保留不可访问,所以这时栈增长到它的最大极限,为了让应用程序知道栈将用完,MiCheckForUserStackOverflow函数返回STATUS_STACK_OVERFLOW,触发栈溢出异常。
如果某一次栈分配需要的空间特别大时,超过了一个页面,这是这个栈帧的某些部分就有可能一下子被分配到保护页之外的未提交空间,这便导致访问违例。为了防止这种情况的发生,对于要分配较大(超过一个页面)的栈空间时,编译器会自动调用一个分配检查函数把超过一个页面的分配分成多次,VC编译器使用的分配检查函数名叫_chkstk。对于一个页面的栈分配,分配检查函数会按照每次不超过一个页面大小逐渐调整栈指针,每调整一次,它会访问一次新分配空间中的内容,称为Probe,目的是触发栈保护页平移,让栈增长。页面大小因处理器不同可能有所变化,x86和x64都是4KB,IA64是8KB。
VC编译器会对变量进行检查工作,主要分为三个部分,一是在分配局部变量时编译器会为每个局部变量多分配8个字节的额外空间(前后各4个字节),用作屏障字段,它和变量尾部的因为内存对齐而分配的补足字节都会被0xCC ( INT 3 指令的机器码) 所填充,这些0xCC称为栅栏字节。二是为了在运行期仍能够准确知道每个变量的长度、位置和名称,编译器会产生一个变量描述表,用来记录局部变量的详细信息。第三,在函数返回前,调用_RTC_CheckStackVars函数,根据变量描述表逐一检查其中包含的每个变量,如果发现变量前后的栅栏字节发生变化则报告检查失败。