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. Linux chmod命令详解

    Linux chmod命令详解 chmod----改变一个或多个文件的存取模式(mode)   chmod [options] mode files   只能文件属主或特权用户才能使用该功能来改变文件 ...

  2. NYOJ 58 最少步数

    最少步数 时间限制:3000 ms  |  内存限制:65535 KB 难度:4   描述 这有一个迷宫,有0~8行和0~8列: 1,1,1,1,1,1,1,1,1 1,0,0,1,0,0,1,0,1 ...

  3. apple store链接格式文档

    备份一下: The app on Appstore has specific URL format http://itunes.apple.com/[country-code]/app/[app-na ...

  4. jquery只能输入数字方法

    本方法为验证文本框的输入内容,如果输入的是数字,则提示"√".否则提示“必填,且只能输入数字字符”.在线体验效果:http://keleyi.com/keleyi/phtml/zz ...

  5. css百宝箱

    关于css百宝箱? 在前端学习中,总会遇到零星的知识点,小技巧,这些知识点小到不至于用一片博客写出来,遇到时网上查询一下或许也能搞定,但不一定能记住,所以这篇博客就用来记录那些散落的知识点,积少成多, ...

  6. javascript的错误处理

    1 onerror事件,实例代码如下: <%@ Page Language="C#" AutoEventWireup="true" CodeBehind= ...

  7. Git的安装和使用记录

    Git是目前世界上最先进的分布式版本控制系统(没有之一),只用过集中式版本控制工具的我,今天也要开始学习啦.廖雪峰的git教程我觉得很详细了,这里记录一下步骤以及我终于学会用Markdown了,真的是 ...

  8. has_many :through VS has_and_belongs_to_many

    user role has_and_belongs_to_many role.destroy:  关联表user_roles先删除记录,再role删除. has_many :through user. ...

  9. 【转】IOS中的release和nil

    nil和release的作用: nil就是把一个对象的指针置为空,只是切断了指针与内存中对象的联系:而release才是真正通知内存释放这个对象. 所以nil并没有释放内存,只有release才回真正 ...

  10. iOS 学习 - 8 TableViewCell 自适应高度

    思路:计算文字的高度,存进数组 加注:存在中文,需要加一行文字的高度,也就是 font 主要代码 #pragma mark -- UITableViewDelegate - (CGFloat)tabl ...