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[] = {}; //分配栈空间
if (recurse){
recursive(recurse);
}
} //下标越界错误
void ArrayErr()
{
int iArray[] = { , };
iArray[] = ; //下标越界,无法恢复
} 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 = , iRet = ;
for (int i = ; i < ;i++){
_tprintf(_T("第%d次循环\n"), i + );
__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 ;
}

16.2 C/C++运行库的栈检查函数

(1)栈检查函数的由来——上面所述的调拨栈空间的策略看似“无懈可击”,可是“暗藏漏洞”。先看下面这段代码:

void SomeFunction(){
int nValues[];
nValues[] = ;//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[] = ; //剩下需要调拨的字节数
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 == ) ? : (uNum + Sum(uNum - )));
} //异常处理过滤函数
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), );
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, , SumThreadFunc,
(PVOID)(UINT_PTR)uSum,,&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
// TEXTINCLUDE
BEGIN
"resource.h\0"
END TEXTINCLUDE
BEGIN
"#include ""winres.h""\r\n"
"\0"
END TEXTINCLUDE
BEGIN
"\r\n"
"\0"
END #endif // APSTUDIO_INVOKED /////////////////////////////////////////////////////////////////////////////
//
// Dialog
// IDD_SUMMATION DIALOGEX , , ,
STYLE DS_SETFONT |DS_CENTER| WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "累加求和"
FONT , "宋体", , , 0x86
BEGIN
LTEXT "从0累加到&x,请在这里输入x:",IDC_STATIC,,,,
EDITTEXT IDC_SUMNUM,,,,,ES_AUTOHSCROLL
DEFPUSHBUTTON "计算(&c)",IDC_CALC,,,,
LTEXT "答案:",IDC_STATIC,,,,
LTEXT "?",IDC_ANSWER,,,,
END /////////////////////////////////////////////////////////////////////////////
//
// DESIGNINFO
// #ifdef APSTUDIO_INVOKED
GUIDELINES DESIGNINFO
BEGIN
IDD_SUMMATION, DIALOG
BEGIN
LEFTMARGIN,
RIGHTMARGIN,
TOPMARGIN,
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线程栈的更多相关文章

  1. 第11章 Windows线程池(1)_传统的Windows线程池

    第11章 Windows线程池 11.1 传统的Windows线程池及API (1)线程池中的几种底层线程 ①可变数量的长任务线程:WT_EXECUTELONGFUNCTION ②Timer线程:调用 ...

  2. Windows核心编程:第11章 Windows线程池

    Github https://github.com/gongluck/Windows-Core-Program.git //第11章 Windows线程池.cpp: 定义应用程序的入口点. // #i ...

  3. 《windows核心编程系列》十五谈谈windows线程栈

    谈谈windows线程栈. 当系统创建线程时会为线程预订一块地址空间区域,注意仅仅是预订.默认情况下预定的这块区域的大小是1MB,虽然预订这么多,但是系统并不会给全部区域调拨物理存储器.默认情况下,仅 ...

  4. 第11章 Windows线程池(2)_Win2008及以上的新线程池

    11.2 Win2008以上的新线程池 (1)传统线程池的优缺点: ①传统Windows线程池调用简单,使用方便(有时只需调用一个API即可) ②这种简单也带来负面问题,如接口过于简单,无法更多去控制 ...

  5. 第11章 Windows线程池(3)_私有的线程池

    11.3 私有的线程池 11.3.1 创建和销毁私有的线程池 (1)进程默认线程池 当调用CreateThreadpoolwork.CreateThreadpoolTimer.CreateThread ...

  6. 《Cracking the Coding Interview》——第16章:线程与锁——题目6

    2014-04-27 20:25 题目:关于java中标有synchronized的成员方法? 解法:这代表同一个对象实例的synchronized方法不能被多个线程同时调用.注意有这么多个地方都加粗 ...

  7. 《Cracking the Coding Interview》——第16章:线程与锁——题目5

    2014-04-27 20:16 题目:假设一个类Foo有三个公有的成员方法first().second().third().请用锁的方法来控制调用行为,使得他们的执行循序总是遵从first.seco ...

  8. 《Cracking the Coding Interview》——第16章:线程与锁——题目2

    2014-04-27 19:14 题目:如何测量上下文切换的时间? 解法:首先,上下文切换是什么,一搜就知道.对于这么一个极短的时间,要测量的话,可以通过放大N倍的方法.比如:有A和B两件事,并且经常 ...

  9. 《Cracking the Coding Interview》——第16章:线程与锁——题目1

    2014-04-27 19:09 题目:线程和进程有什么区别? 解法:理论题,操作系统教材上应该有很详细的解释.我回忆了一下,写了如下几点. 代码: // 16.1 What is the diffe ...

随机推荐

  1. ahjesus fstab修改错误了如何修复

    fstab修改错误了如何修复   当你不小心把磁盘表输入错误以后,系统总是让你按ctrl+D重新启动或者输入密 码进入shell,你输入密码登陆后,   编辑文件是只读的,执行下面的命令后就可以编辑了 ...

  2. virtualenv and virtualenvwrapper on Ubuntu 14.04

    In this post I’ll go over my attempt to setup virtual environments for Python development. Most Pyth ...

  3. 【读书笔记】iOS-编码对象

    Cocoa具备一种机制来将对象自身转换为某种格式并保存到磁盘中.对象可以将它们的实例变量和其他数据编码为数据块,然后保存到磁盘中.以后将这些数据块读回到内存中,并且还能基于保存的数据创建新对象.这个过 ...

  4. 【读书笔记】iOS-验证应用内支付的凭证注意事项

    1,简单来说,越狱后的手机由于没有沙盒作为保护,黑客可以对系统进行任意的修改,所以,在支付过程中,苹果返回的已付款成功的凭证可能是伪造的.客户端拿到付款凭证之后,还需要将凭证上传到自己的服务器,进行二 ...

  5. 带删除的EditText

    在安卓开发中EditText是比较常用的控件之一,那我们平常看到EditText填写了内容之后右边会出现一个删除的按钮,这样可以方便用户对其中文本清空操作,是非常人性化的,我们可以重写EditText ...

  6. C#照片批量压缩小工具

    做了一个照片批量压缩工具,其实核心代码几分钟就完成了,但整个小工具做下来还是花了一天的时间.中间遇到了大堆问题,并寻求最好的解决方案予以解决.现在就分享一下这个看似简单的小工具所使用的技术. 软件界面 ...

  7. Android开发中 .9.png格式图形设计:

    Android .9.png设计 宿舍大神在做android项目,有幸得知.9.png的图形格式. 不知道大家是否注意过聊天气泡和锁屏时随着你文字的增多和你的滑动而跟着变化并且分辨率没有变低的图形?是 ...

  8. html点击按钮 弹出 多选择窗口级联下拉复选

    参考代码 代码示例1: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http:/ ...

  9. 小心sae的jvm异常导致的Error 404 – Not Found.No context on this server matched or handled this request.

    本来用着sae好好的,结果第二天部署的应用突然不好使了,各种Error 404 – Not Found.No context on this server matched or handled thi ...

  10. 迅为iTOP-4418开发板兼容八核6818开发板介绍

    核心板介绍 三星四核S5P4418与八核6818完美兼容 1GB内存/2GB内存可选 电源管理:AXP228,支持动态调频,超低功耗 核心板引出脚最全:四组连接器共320个PIN脚 核心板连接器高度仅 ...