第16章 Windows线程栈

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)线程栈的工作原理

第16章 Windows线程栈_第1张图片 

【初始状态】

  ①设页面大小为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程序】——演示栈溢出及如何恢复

第16章 Windows线程栈_第2张图片

#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示例程序】展示如何使用异常过滤程序及异常处理程序来从栈溢出中得体恢复

第16章 Windows线程栈_第3张图片

/************************************************************************
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

 

你可能感兴趣的:(第16章 Windows线程栈)