系统在用户进程的地址空间中预订区域的情况有:
分配进程环境块、分配线程环境块以及分配线程栈。
下面主要讲解线程栈的分配。
当系统创建线程的时候,会为线程栈预订一块地址空间区域(每个线程都有自己的栈),并给区域调拨一些物理存储器。在默认情况下:预订1MB的地址空间,调拨2个页面。
在构建应用程序时开发人员可以通过两种方法来改变该默认值:
(1)使用Microsoft C++编译器的/F选项。
(2)使用Microsoft C++链接器的/STACK选项:
/Freserve
/STACK:reserve[,commit]
下图显示了一台页面大小为4KB的机器上的线程栈的地址空间区域(基地址为0x08000000)。该线程栈的地址空间区域和所有调拨给该区域的物理存储器都具有PAGE_READWRITE保护属性。
图一 线程栈的地址空间区域最初创建时的样子
在预订地址空间区域后,系统会给区域顶部(地址最高)的两个页面调拨物理存储器。在线程开始执行前,系统会把线程栈的指针指向区域顶部的两个页面的末尾,这个页面就是线程开始使用栈的地方。区域顶部往下的第二个页面称为防护页面(guard page),随着线程调用越来越多的函数,调用树也越来越深,线程也需要越来越多的栈空间。
当线程试图访问防护页面中的内存时,系统会得到通知。这是系统会先给防护页面下面的那个页面调拨存储器,接着去除防护页面的PAGE_GUARD保护属性标志,然后给刚才调拨的存储页指定PAGE_GUARD保护属性标志。这使得系统能够在线程需要的时候才增大栈存储器的大小。如果线程栈的调用树不断加深,那么栈的地址空间区域看起来会是图二这样。
图二 即将用尽的栈地址空间区域
现在假设线程的调用树非常深,CPU的栈指针寄存器指向的内存地址为0x08003004.现在,当线程调用另一个函数时,系统必须调拨更多的物理存储器。但是这个时候的做法和给区域中其他部分调拨物理存储器不同。目前栈的地址空间区域如图三所示。
图三 已用尽的栈地址空间区域
这个时候,系统会去除地址为0x08002000的页面的PAGE_GUARD保护属性标志,然后给地址为0x08001000的页面调拨物理存储器。但是,系统不会给刚调拨的物理存储器(0x08001000)指定防护属性。这意味着栈的地址空间区域已经放满了所能容纳的所有物理存储器。系统永远不会给最底部的那个页面调拨存储器。
当系统给0x08001000的页面调拨物理存储器时,会执行一个额外的操作-抛出E
X C E P T I O N _ S TA C K _ O V E R F L O W 异常。通过使用SEH(结构化异常处理),系统会在发生这一情况时通知我们的程序,使得程序能够得体地从这一异常情况下恢复。
如果在出现堆栈溢出异常条件之后,线程继续使用该堆栈,那么在0
x 0 8 0 0 1 0 0 0地址上的页面内存均将被使用,同时,该线程将试图访问从0 x 0 8 0 0 0 0 0 0页面中的内存。当该线程访问尚未调拨物理存储器的内存时,系统就会引发一个访问违规异常。如果在线程访问该堆栈时引发了这个访问违规异常条件,线程就会陷入很大的麻烦之中。这时,系统就会接管控制权,并终止进程的运行—不仅终止线程的运行,而切终止整个进程的运行。
系统为什么始终不给栈地址空间区域最底部的页面调拨物理存储器?
目的是为了保护进程使用的其他数据,使它们不会以为意外的内存写越界而遭到破坏。
因为地址空间区域的地址0x07ff000处级位于0x08000000下方的一个页面可能已经调拨物理存储器。如果地址为0x08000000即最底部的页面也调拨了物理存储器,那么系统就无法捕捉到线程对栈外区域的访问。如果栈的增长越过了所预定的区域,那么线程就会覆盖空间中的其他数据。
另一种很难找到的缺陷是栈下溢(stack
underflow)。看一个例子代码:
int WINAPI WinMain (HINSTANCE hInstExe, HINSTANCE, PTSTR pszCmdLine, int nCmdShow) { BYTE aBytes[100]; aBytes[10000] = 0; // Stack underflow return(0); }
代码会试图访问线程栈之外的内存。编译器和链接器无法发现代码中此类错误。这条语句可能引发访问违规,也可能不会,因为紧接着线程栈后面可能有另一块已经调拨的地址空间区域。
C/C++运行库的栈检查函数
C/C++的运行库中有一个栈检查函数,编译器会根据目标平台的页面大小自动插入代码来调用StackCheck函数。其目的是为了确保给线程栈调拨了物理存储器。
看下面一个例子代码:
void SomeFunction () { int nValues[4000]; // Do some processing with the array. nValues[0] = 0; // Some assignment }
局部变量需要占用大量内存,需要至少16000字节的栈空间来存放。通常情况下,编译器会直接把CPU的栈指针减去16000字节。到那时,除非程序试图访问其中的数据,否则系统是不会给这块区域调拨物理存储器的。
在页面大小为4KB或者8KB的系统中,这个限制会产生问题:如果第一次访问的地址要低于防护页面,那么线程会访问尚未调拨的内存并引发访问违规。因此,为了保证类似上面的代码正常运行,编译器需要插入一段代码来调用C运行库的栈检查函数。
栈检查函数通常由编译器实现,下面是一个描述栈检查函数的伪代码:
// The C run-time library knows the page size for the target system. #ifdef _M_IA64 #define PAGESIZE (8 * 1024) // 8-KB page #else #define PAGESIZE (4 * 1024) // 4-KB page #endif void StackCheck(int nBytesNeededFromStack) { // Get the stack pointer position. // At this point, the stack pointer has NOT been decremented // to account for the function's local variables. PBYTE pbStackPtr = (CPU's stack pointer); while (nBytesNeededFromStack >= PAGESIZE) { // Move down a page on the stack--should be a guard page. pbStackPtr -= PAGESIZE; // Access a byte on the guard page--forces new page to be // committed and guard page to move down a page. pbStackPtr[0] = 0; // Reduce the number of bytes needed from the stack. nBytesNeededFromStack -= PAGESIZE; } // Before returning, the StackCheck function sets the CPU's // stack pointer to the address below the function's // local variables. }
示例代码:
下面的代码展示了使用异常过滤程序及异常处理程序来从栈溢出中得体恢复并继续运行。
#include <Windows.h> #include <process.h> #include <iostream> using namespace std; //调用C运行库的_beginthreadex函数 typedef unsigned (__stdcall *PTHREAD_START) (void *); #define chBEGINTHREADEX(psa, cbStackSize, pfnStartAddr, \ pvParam, dwCreateFlags, pdwThreadId) \ ((HANDLE)_beginthreadex( \ (void *) (psa), \ (unsigned) (cbStackSize), \ (PTHREAD_START) (pfnStartAddr), \ (void *) (pvParam), \ (unsigned) (dwCreateFlags), \ (unsigned *) (pdwThreadId))) UINT Sum(UINT uNum) { return ((uNum == 0) ? 0 : (Sum(uNum - 1) + uNum)); } //SEH过滤器表达式 LONG WINAPI FilterFunc(DWORD dwExceptionCode) { return((dwExceptionCode == STATUS_STACK_OVERFLOW) ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH); } //执行求和的线程函数 DWORD WINAPI SumThreadFunc(PVOID pvParam) { // The parameter pvParam, contains the number of integers to sum. UINT uSumNum = PtrToUlong(pvParam); // uSum contains the summation of the numbers from 0 through uSumNum. // If the sum cannot be calculated, a sum of UINT_MAX is returned. UINT uSum = UINT_MAX; __try { //在SEH块中执行计算的函数 uSum = Sum(uSumNum); } __except (FilterFunc(GetExceptionCode())) { // If we get in here, it's because we have trapped a stack overflow. // We can now do whatever is necessary to gracefully continue execution // This sample application has nothing to do, so no code is placed // in this exception handler block. } // The thread's exit code is the sum of the first uSumNum // numbers, or UINT_MAX if a stack overflow occurred. return (uSum); } int main() { DWORD dwThreadId; UINT uSum; while(1) { cout << "输入一个整数:" << endl; cin >> uSum; HANDLE hThread = chBEGINTHREADEX(NULL, 0, SumThreadFunc, (PVOID) (UINT_PTR) uSum, 0, &dwThreadId); WaitForSingleObject(hThread,INFINITE); GetExitCodeThread(hThread, (PDWORD) &uSum); CloseHandle(hThread); if (UINT_MAX == uSum) { cout << "ERROR : STACKOVERFLOW,The number is too big" << endl; } else cout << "The result is " << uSum << endl; } }
运行结果(VS2008):