zoukankan      html  css  js  c++  java
  • 第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)线程栈的工作原理

     

    【初始状态】

      ①设页面大小为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次循环
    "), i + 1);
            __try{
                //模拟栈溢出
                //ArrayErr(); //下标越界,无法检测出来,所以不会抛出异常。
                recursive(recurse);
    
            }__except(stack_overflow_exception_filter(GetExceptionCode())){
                _tprintf(_T("恢复栈溢出....
    "));
                iRet = _resetstkoflw();
            }
    
            if (!iRet){
                _tprintf(_T("恢复失败
    "));
                break;    
            } else{
                _tprintf(_T("恢复成功
    "));
            }
        }
        _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"
    END
    
    2 TEXTINCLUDE 
    BEGIN
        "#include ""winres.h""
    "
        ""
    END
    
    3 TEXTINCLUDE 
    BEGIN
        "
    "
        ""
    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
  • 相关阅读:
    3.16
    3.8
    3.7
    3.6
    3.5
    3.3
    3.2
    《机器学习十讲》学习报告一
    机器学习-集成算法
    机器学习-基于核的算法
  • 原文地址:https://www.cnblogs.com/5iedu/p/4888094.html
Copyright © 2011-2022 走看看