16.1 线程栈及工作原理
(1)线程栈简介
①系统在创建线程时,会为线程预订一块地址空间(即每个线程私有的栈空间),并调拨一些物理存储器。默认情况下,预订1MB的地址空间并调拨两个页面的存储器。
②调整线程栈的默认大小可以使用编译选项或#pragma指令,具体用法视编译器不同,VC下可以使用 /Fnewsize 编译选项设置默认栈大小,其中newsize是以字节为单位,也可以使用/STACK:reserve[,commit]连接选项,使用#pragma指令的样式如下:#pragma comment(linker, "/STACK:reserve,commit") ,其中的reserve和commit均以字节为单位。这些信息会被写入.exe或.dll文件的PE文件头中。
③也可以在调用CreateThread或_beginthreadex函数时,给dwStackSize参数指定一个值来改变栈的大小,如果该参数为0时,表示PE文件头指定的大小。
(2)线程栈的工作原理
【初始状态】
①设页面大小为4KB,栈大小为1MB。图中线程栈的基地址为0x80000000,所有己调拨的页面都具有PAGE_READWRITE保护属性
②初始化时,栈顶指针ESP如上图所示(接过0x8100 0000),这个页面是线程开始使用栈的地方。往下看,第2个页面为“防护页面(guard page)”
③随着线程调用越来越多的函数,调用树也越来越深,线程所需的栈空间也越来越多。
【栈即将用尽状态】
①当线程试图访问“防护页面”的内存时,系统会得到通知,这时系统会先给“防护页面”下面的那个页面调拨物理存储器,接着去除当前“防护页面”的PAGE_GUARD保护标志,然后给刚调拨的存储页指定PAGE_GUARD保护属性。
②该项技术使用系统能够在线程需要的时候才增大栈存储器的大小。如果线程的调用树不断加深,那么栈的地址空间区域将很快被占满。
【栈满时的状态】
①如果线程的调用树非常深,CPU的ESP指针指向了0x0800 3004。此时,当线程调用另一个函数时,就必须调拨更多的物理存储器。但是当给0x0800 1000页面调拨物理存储器时。它的做法和给区域其他部分调拨物理存储器有所不同。
②首先会去除0x0800 2000页面的PAGE_GUARD标志,然后给0x0800 1000页面调拨。但区别在于,此时不会给0x0800 1000指定防护属性。这意味着栈的地址空间区域己经放满所能容纳的所有物理存储器。
③当系统给0x0800 1000页面调拨物理存储器时,会执行一个额外操作——抛出EXCEPTION_STACK_OVERFLOW异常,以通知应用程序,从而使程序能够得体地从这异常情况下恢复。(这里提供一种机制让线程栈溢出时,有补救的措施)
④但是,如果线程在引发栈溢出异常后继续使用栈,那它会用尽0800 1000页面,并试图访问地址0x800 0000页面的内存。但这个页面被设计为“不可调拨的页面”,所以会抛出访问违规异常。此时系统会收回控制权并弹出错误,从而结束整个进程(而不仅仅是线程!)。如避免这种情况,应用程序可以调用SetThreadStackGuarante函数,以确保Windows在终止进程之前,地址空间中还有指定数量的内存,使应用程序抛出EXCEPTION_STACK_OVERFLOW异常以便让用户自行决定如何处理和恢复。
(3)线程栈溢出时的恢复
①当线程访问最后一个防护页面时,系统会抛出EXCEPTION_STACK_OVERFLOW异常。如果线程捕获了该异常并继续执行,那么系统将不会在同一个线程中再次抛出异常,因为后面再也没有防护页面了。
②如果希望在同一线程中继续收到EXCEPTION_STACK_OVERFLOW异常,那么应用程序必须重置防护页面。只需调用运行库的_resetstkoflw函数(在malloc.h中定义)
【StackOverflow程序】——演示栈溢出及如何恢复
#include <windows.h> #include <tchar.h> #include <strsafe.h> #include <locale.h> #include <malloc.h> //调用_resetstkoflow函数 //递归 void recursive(int recurse){ int iArray[2000] = {}; //分配栈空间 if (recurse){ recursive(recurse); } } //下标越界错误 void ArrayErr() { int iArray[] = { 3, 4 }; iArray[10] = 1; //下标越界,无法恢复 } int stack_overflow_exception_filter(int exception_code){ if (exception_code == EXCEPTION_STACK_OVERFLOW){ //执行__except后{}中的代码, 即执行异常处理代码, 不返回到__try中 return EXCEPTION_EXECUTE_HANDLER; //EXCEPTION_CONTINUE_EXECUTION,返回__try块中的异常代码处继续执行,即异常已被正常处理 } else{ //继续查找,即本__except块不能处理此异常 return EXCEPTION_CONTINUE_SEARCH; } } int _tmain(){ _tsetlocale(LC_ALL, _T("chs")); int recurse = 1, iRet = 0; for (int i = 0; i < 10;i++){ _tprintf(_T("第%d次循环\n"), i + 1); __try{ //模拟栈溢出 //ArrayErr(); //下标越界,无法检测出来,所以不会抛出异常。 recursive(recurse); }__except(stack_overflow_exception_filter(GetExceptionCode())){ _tprintf(_T("恢复栈溢出....\n")); iRet = _resetstkoflw(); } if (!iRet){ _tprintf(_T("恢复失败\n")); break; } else{ _tprintf(_T("恢复成功\n")); } } _tsystem(_T("PAUSE")); return 0; }
16.2 C/C++运行库的栈检查函数
(1)栈检查函数的由来——上面所述的调拨栈空间的策略看似“无懈可击”,可是“暗藏漏洞”。先看下面这段代码:
void SomeFunction(){ int nValues[4000]; nValues[0] = 0;//assign a value }
在32位系统中,这个函数至少需要4000*sizeof(int)=16000字节,当第1次访问的地址低于防护页面时[见线程栈运行时状态图1](如nValues[0])。index为0的元素在哪里呢?在栈的低地址!如果默认1MB的栈空间分配的话,nValues[0]将访问尚未调拨的空间(因为创建线程栈时,初始化时只调拨两个页面,而低地址端的页面是尚未调拨的。注意栈的生长方向)。
(2)C/C++栈检查函数
①为了解决上述问题,编译器会自动插入栈检查代码。编译器能够计算出函数所需要的栈空间,如果所需要的空间大于一个页面的大小,编译器就会为函数插入检查代码。检查代码的原理很简单:每次试图访问下一个页面中的某个地址,以使系统自动为它分配调拨内存,直到需要的栈空间都满足为止。当然如果预设的栈空间不够的话,还是会先引发溢出异常。
②栈检查函数伪代码——由编译器开发商用汇编语言来实现!
//C运行库知道目标系统的页面大小 #ifdef _M_ALPHA #define PAGESIZE (8*1024) //8-KB page #else #define PAGESIZE (4*1028) //4-KB page #endif void StackCheck(int nBytesNeededFromStack) { //获得栈顶指针,此时栈顶指针还没减去“局部变量”所示的空间大小 PBYTE pbStackPtr = (CPU's stack pointer); //CPU栈顶指针 while(nBytesNeededFromStack >= PAGESIZE) { //将栈顶指针移到PAGE_GUARD页面 pbStackPtr -=PAGESIZE; //访问1个字节,以强迫系统调拨下一个页面 pbStackPtr[0] = 0; //剩下需要调拨的字节数 nBytesNeededFromStack -= PAGESIZE; } //用返回之前,StatckCheck函数将CPU的栈顶指针设置在调用函数 //的局部变量下 }
【Summation示例程序】展示如何使用异常过滤程序及异常处理程序来从栈溢出中得体恢复
/************************************************************************ Module: Summation.cpp Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre ************************************************************************/ #include "../../CommonFiles/CmnHdr.h" #include "resource.h" #include <tchar.h> ////////////////////////////////////////////////////////////////////////// //为了演示栈溢出,这里的求和公式不用高斯公式,而是用递归调用来实现 //Sum函数应用举例 //uNum: 0 1 2 3 4 5 6 7 8 9... //Sum: 0 1 3 6 10 15 21 28 36 45... UINT Sum(UINT uNum){ //递归调用Sum函数 return ((uNum == 0) ? 0 : (uNum + Sum(uNum - 1))); } //异常处理过滤函数 LONG WINAPI FilterFunc(DWORD dwExceptionCode){ return (dwExceptionCode == STATUS_STACK_OVERFLOW) ? EXCEPTION_EXECUTE_HANDLER : //执行__except后{}中的代码, 即执行异常处理代码. EXCEPTION_CONTINUE_SEARCH; //继续查找,即本__except块不能处理此异常 } ////////////////////////////////////////////////////////////////////////// BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam){ chSETDLGICONS(hwnd, IDI_SUMMATION); //不接受超过9位的数字 Edit_LimitText(GetDlgItem(hwnd, IDC_SUMNUM), 9); return TRUE; } ////////////////////////////////////////////////////////////////////////// //该独立的线程负责计算总和,使用独立线程的原因: //1.可以获得线程私有的1MB地址空间 //2.每个线程只能有一次栈溢出时的通知 //3.当线程退出时,系统会自动回收调拨给线程栈的物理存储器 DWORD WINAPI SumThreadFunc(PVOID pvParam){ //pvParam参数表示要累加到的数字 UINT uSumNum = PtrToUlong(pvParam); //uSum表示从0到uSumNum的累加总和 UINT uSum = UINT_MAX; __try{ uSum = Sum(uSumNum); }__except(FilterFunc(GetExceptionCode())){ //如果函数执行到这里,表示己经捕获到一个栈溢出的异常 //这里我们可以进行一个异常处理以便得体地退出。 //因这是一个示例程序,这里我们不做任何事情 } //线程的退出代码为最终的求和结果,如果为UINT_MAX则表示栈溢出! return uSum; } ////////////////////////////////////////////////////////////////////////// void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtrl, UINT codeNotity){ switch (id) { case IDCANCEL: EndDialog(hwnd, id); break; case IDC_CALC: //获取用户输入的x值 BOOL bSuccess = TRUE; UINT uSum = GetDlgItemInt(hwnd, IDC_SUMNUM, &bSuccess, FALSE); if (!bSuccess){ MessageBox(hwnd, TEXT("请输入一个有效的数字!"), TEXT("非法输入..."),MB_ICONINFORMATION | MB_OK); SetFocus(GetDlgItem(hwnd, IDC_SUMNUM)); break; } //创建一个线程(拥用自己的线程栈)来负责执行累加计算 DWORD dwThreadId; HANDLE hThread = chBEGINTHREADEX(NULL, 0, SumThreadFunc, (PVOID)(UINT_PTR)uSum,0,&dwThreadId); //等待线程结束 WaitForSingleObject(hThread, INFINITE); //获取线程退出代码 GetExitCodeThread(hThread, (PDWORD)&uSum); //允许关于线程内核对象 CloseHandle(hThread); //显示计算结果 if (uSum == UINT_MAX){ //如果结果是UINT_MAX,表示发生了栈溢出 SetDlgItemText(hwnd, IDC_ANSWER, TEXT("栈溢出错误!")); chMB("数字太大,请输入一个较小的数字!"); } else{ //计算成功 SetDlgItemInt(hwnd, IDC_ANSWER, uSum, FALSE); } break; } } ////////////////////////////////////////////////////////////////////////// INT_PTR WINAPI Dlg_Proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam){ switch (uMsg){ chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog); chHANDLE_DLGMSG(hwnd, WM_COMMAND, Dlg_OnCommand); } return FALSE; } ////////////////////////////////////////////////////////////////////////// int WINAPI _tWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPTSTR lpCmdLine, _In_ int nShowCmd) { DialogBox(hInstance, MAKEINTRESOURCE(IDD_SUMMATION), NULL, Dlg_Proc); }
//resource.h
//{{NO_DEPENDENCIES}} // Microsoft Visual C++ 生成的包含文件。 // 供 16_Summation.rc 使用 // #define IDD_SUMMATION 101 #define IDI_SUMMATION 102 #define IDC_SUMNUM 1000 #define IDC_CALC 1001 #define IDC_ANSWER 1002 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 103 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1001 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif
//Summation.rc
// Microsoft Visual C++ generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // 中文(简体,中国) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS) LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h\0" END 2 TEXTINCLUDE BEGIN "#include ""winres.h""\r\n" "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Dialog // IDD_SUMMATION DIALOGEX 18, 18, 163, 35 STYLE DS_SETFONT |DS_CENTER| WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "累加求和" FONT 10, "宋体", 400, 0, 0x86 BEGIN LTEXT "从0累加到&x,请在这里输入x:",IDC_STATIC,5,4,112,11 EDITTEXT IDC_SUMNUM,117,2,40,13,ES_AUTOHSCROLL DEFPUSHBUTTON "计算(&c)",IDC_CALC,4,19,56,12 LTEXT "答案:",IDC_STATIC,68,21,30,8 LTEXT "?",IDC_ANSWER,104,21,56,8 END ///////////////////////////////////////////////////////////////////////////// // // DESIGNINFO // #ifdef APSTUDIO_INVOKED GUIDELINES DESIGNINFO BEGIN IDD_SUMMATION, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 162 TOPMARGIN, 7 END END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_SUMMATION ICON "Summation.ico" #endif // 中文(简体,中国) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED