用Windows 结构化异常处理及虚拟内存的简单程序
2012-06-29 16:19
281 查看
前段时间再次基本把《Windows核心编程》看完了一次(第一次是看的电子版的,这次是印刷版的),对书中描述的Windows系统虚拟内存管理和结构化异常处理的印象比较深。那时候工作上也没什么事情,于是就写了个小程序来练手。今天偶尔把它翻出来了,就改了改,作为今天的一篇技术方面的日志发表了吧。
#define UNICODE
#define _UNICODE
#include <windows.h>
#include <windowsx.h>
#include <tchar.h>
#include <process.h>
#define USE_RECURSION // 使用递归
#ifndef USE_RECURSION // 如果不使用递归
static int *g_pStack = 0;
static int g_nDepth = 0;
#endif
static void TStringPrintf(const TCHAR *format,...)
{
HANDLE hStdOut;
TCHAR szInfo[1024];
va_list list;
hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
if (hStdOut == INVALID_HANDLE_VALUE) return;
va_start(list,format);
wvsprintf(szInfo,format,list);
va_end(list);
WriteConsole(hStdOut,szInfo,_tcslen(szInfo),NULL,NULL);
}
static long ExptFilter(EXCEPTION_POINTERS* pExp)
{
// 递归(嵌套)调用层数太多,线程堆栈溢出
if (pExp->ExceptionRecord->ExceptionCode == EXCEPTION_STACK_OVERFLOW)
{
//MessageBox(NULL,TEXT("堆栈溢出异常"),TEXT("异常"),MB_OK | MB_ICONWARNING);
return EXCEPTION_EXECUTE_HANDLER;
}
// 还未提交物理存储器,拒绝访问
else if (pExp->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)
{
TCHAR szInfo[100];
wsprintf(szInfo,TEXT("%s 地址 0x%X 发生拒绝访问异常!"),
(pExp->ExceptionRecord->ExceptionInformation[0] ? TEXT("写") : TEXT("读")),
pExp->ExceptionRecord->ExceptionInformation[1]);
MessageBox(NULL,szInfo,TEXT("异常"),MB_OK | MB_ICONWARNING);
VirtualAlloc((LPVOID)(pExp->ExceptionRecord->ExceptionInformation[1]),
29,MEM_COMMIT,PAGE_READWRITE);
return EXCEPTION_CONTINUE_EXECUTION;
}
else
{
return EXCEPTION_EXECUTE_HANDLER;
}
}
static int NSum(int n)
{
#ifndef USE_RECURSION
if (0 == n)
return n;
else
{
int x,sum;
for(x = n; x >= 0; x--)
{
g_pStack[g_nDepth] = x;
g_nDepth += 1;
}
sum = 0;
for(x = g_nDepth - 1; x >= 0; x--)
sum += g_pStack[x];
return sum;
}
#else
if (0 == n)
return n;
else
return (n + NSum(n-1));
return -1;
#endif
}
static DWORD CALLBACK ThreadProc(LPVOID param)
{
__try
{
__try
{
int sum;
#ifndef USE_RECURSION
g_pStack = VirtualAlloc(0,sizeof(int) * (int)param,MEM_RESERVE,PAGE_READWRITE);
g_nDepth = 0;
#endif
sum = NSum((int)param);
TStringPrintf(TEXT("sum(%d) = %d\n"),(int)param,sum);
}
__except(ExptFilter(GetExceptionInformation()))
{
if (GetExceptionCode() == EXCEPTION_STACK_OVERFLOW)
MessageBox(NULL,TEXT("堆栈溢出异常"),TEXT("异常"),MB_OK | MB_ICONWARNING);
else
MessageBox(NULL,TEXT("未预期的异常"),TEXT("异常"),MB_OK | MB_ICONWARNING);
}
}
__finally
{
MessageBox(NULL,TEXT("Finally 总是要执行的."),TEXT("Finally"),MB_OK | MB_ICONINFORMATION);
}
return 0;
}
//int _tmain(int argc,TCHAR *argv[])
int CALLBACK _tWinMain(HINSTANCE hPrev,HINSTANCE hInst,LPTSTR cmdLine,int show)
{
HANDLE hThread;
int n = 11800;
hThread = (HANDLE)_beginthreadex(0,0,ThreadProc,(void*)n,0,0);
if (hThread)
WaitForSingleObject(hThread,INFINITE);
CloseHandle(hThread);
return 0;
}
本程序的功能是计算从整数1加到整数n的和。程序使用递归和非递归两种方法来解决此问题:如果定义了USE_RECURSION宏,就使用递归解决方法,否则使用非递归解决方法。下面按照重要程度对程序的知识点进行一些总结。
1 递归解决方法中,当整数n太大时,函数递归调用层数过多,会引起堆栈溢出。程序用SEH(结构化异常处理)捕获此异常。相关代码是第105行左右的except子句。这是结构化异常处理的一般用法,没什么特别的。要注意的是,在异常过滤器函数ExptFilter()中,代码的第38处的MessageBox()函数调用是不正确的。因为在这里,线程已经堆栈溢出了,不能再嵌套地调用任何函数,因为函数调用是需要堆栈支持的(调用前保存返回地址到堆栈中,调用后从堆栈取出返回地址,并跳转到此地址)。然而,在异常处理代码(except子句)中,可以进行函数调用,因为此时系统已经确定要执行异常处理代码,发生异常的函数调用(103行对NSum的调用以及第86行NSum自身的多级递归调用)都已经全部退栈,线程已经从堆栈溢出状态中恢复。
2 非递归解决方法使用虚拟内存保存计算的中间结果,相当于用虚拟内存替换递归方法中的函数调用堆栈。
(1)代码第13和14行定义了非递归方法中用到的全局变量:g_pStack相当于递归调用时候的线程堆栈;g_nDepth是调用的深度。代码第100和101行对这两个变量进行初始化。
(2)代码第66至80行用非递归方法解决从1加到整数n的问题。当然,这样做是很无聊的,这个问题的解决本来是很简单的,这里是为了使用虚拟内存技术才故意这么写的。
(3)由于第100行在初始化g_pStack时,只预留地址空间,而没有提交物理存储器,所以第73行代码在执行时会引起拒绝访问异常。异常过滤器函数ExptFilter()第42行识别出这种异常,给出提示对话框。然后,第51行进行物理存储器的提交。
注意:
a 提交物理存储器时给出的起始地址是pExp->ExceptionRecord->ExceptionInformation[1],而不是pExp->ExceptionRecord->ExceptionAddress。要注意二者的区别:前者是发生拒绝访问异常时,企图访问的地址,即因为企图访问此地址才引起拒绝访问异常的。它的值仅在发生拒绝访问异常时有效。(目前只有拒绝访问异常定义了ExceptionInformation字段,其他类型异常的这个字段没有定义)。后者是引发异常的指令的地址,它对于任何类型的异常都是有效的。总的来说,前者是数据地址,只对拒绝访问异常有意义;后者是代码地址,对任何异常有意义。
b 提交的物理存储器大小是任意给出的29。其实,虚拟空间的预留和提交都是按页进行的,这个值29会自动转变成4KB(一页的大小),系统提交一页物理存储器。
c 提交物理存储器后,发生拒绝访问异常时企图访问的地址变为有效,程序可以访问此地址了,所以第54行处异常过滤器函数ExpFilter()返回EXCEPTION_CONTINUE_EXECUTION,让系统重新执行发生异常的指令。这次执行就不会引起拒绝访问异常了。
非递归方法避免了递归方法由于函数嵌套调用层数过深而引起的,不可恢复的堆栈溢出错误,在n值比较大时也可以正确第解决问题。因为线程的堆栈通常是很小的(默认为1MB),而线程可用的虚拟地址空间可接近2GB(32位Windows中,进程地址空间为4GB,其中约2GB被系统使用)。
当然,创建线程时可以指定线程堆栈大小(修改128行处_beginthreadex调用的第二个参数),但是对于函数的多级递归调用,我们无法准确了解至少要多大堆栈才够用(因为不了解系统函数调用使用堆栈的细节情况)。唯一的方法是指定尽量大的堆栈大小。
变通的方法是把递归的解决方案变成非递归的。不过,虽然从理论上来说,递归的算法都可以转换成非递归的,本程序涉及的问题的非递归解法也很容易实现,但是对于较复杂的问题,非递归解法却不是那么容易构造的。
本程序在使用虚拟地址空间时,采用先预留空间,在必要时才提交物理存储器的策略。这样可以避免不必要地占用系统页面文件空间的问题。本程序通过捕获到拒绝访问异常而判断需要提交物理存储器,还有一种方法是通过为页面设置PAGE_GUARD属性来实现,这个以后有机会再写程序做试验。
以上两点是本文的重点。下面再按照在程序中出现的次序,说说本程序可以总结的一些地方。
3 第17行的TStringPrintf()实现了(Unicode和非Unicode)通用的的printf()函数。C/C++标准库提供的printf()是不能处理Unicode字符串的。为了能处理Unicode,C/C++标准库引入了tchar.h头文件和wchar_t类型。但是,在Visual C++ 6.0中,在程序定义了_UNICODE宏时,使用_tprintf()和wprintf()都不能正确输出,不知道是Visual C++提供的C/C++标准库的bug,还是我的使用方法不对。
4 程序92行的ThreadProc()函数中使用了嵌套的try语句。Windows的结构化异常处理中,try语句只能跟随异常处理子句except或者结束处理子句finally。如果要同时使用except和finally子句,必须使用这种嵌套的形式。
5 第122行处main()和WinMain()函数的定义。Visual C++的编译器可以自动根据程序是否定义了UNICODE宏,以及是否定义了main(),_tmain(),WinMain,_tWinMain()函数来确定程序的类型:是否使用Unicode,是控制台程序还是一般的Windows程序。
#define UNICODE
#define _UNICODE
#include <windows.h>
#include <windowsx.h>
#include <tchar.h>
#include <process.h>
#define USE_RECURSION // 使用递归
#ifndef USE_RECURSION // 如果不使用递归
static int *g_pStack = 0;
static int g_nDepth = 0;
#endif
static void TStringPrintf(const TCHAR *format,...)
{
HANDLE hStdOut;
TCHAR szInfo[1024];
va_list list;
hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
if (hStdOut == INVALID_HANDLE_VALUE) return;
va_start(list,format);
wvsprintf(szInfo,format,list);
va_end(list);
WriteConsole(hStdOut,szInfo,_tcslen(szInfo),NULL,NULL);
}
static long ExptFilter(EXCEPTION_POINTERS* pExp)
{
// 递归(嵌套)调用层数太多,线程堆栈溢出
if (pExp->ExceptionRecord->ExceptionCode == EXCEPTION_STACK_OVERFLOW)
{
//MessageBox(NULL,TEXT("堆栈溢出异常"),TEXT("异常"),MB_OK | MB_ICONWARNING);
return EXCEPTION_EXECUTE_HANDLER;
}
// 还未提交物理存储器,拒绝访问
else if (pExp->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)
{
TCHAR szInfo[100];
wsprintf(szInfo,TEXT("%s 地址 0x%X 发生拒绝访问异常!"),
(pExp->ExceptionRecord->ExceptionInformation[0] ? TEXT("写") : TEXT("读")),
pExp->ExceptionRecord->ExceptionInformation[1]);
MessageBox(NULL,szInfo,TEXT("异常"),MB_OK | MB_ICONWARNING);
VirtualAlloc((LPVOID)(pExp->ExceptionRecord->ExceptionInformation[1]),
29,MEM_COMMIT,PAGE_READWRITE);
return EXCEPTION_CONTINUE_EXECUTION;
}
else
{
return EXCEPTION_EXECUTE_HANDLER;
}
}
static int NSum(int n)
{
#ifndef USE_RECURSION
if (0 == n)
return n;
else
{
int x,sum;
for(x = n; x >= 0; x--)
{
g_pStack[g_nDepth] = x;
g_nDepth += 1;
}
sum = 0;
for(x = g_nDepth - 1; x >= 0; x--)
sum += g_pStack[x];
return sum;
}
#else
if (0 == n)
return n;
else
return (n + NSum(n-1));
return -1;
#endif
}
static DWORD CALLBACK ThreadProc(LPVOID param)
{
__try
{
__try
{
int sum;
#ifndef USE_RECURSION
g_pStack = VirtualAlloc(0,sizeof(int) * (int)param,MEM_RESERVE,PAGE_READWRITE);
g_nDepth = 0;
#endif
sum = NSum((int)param);
TStringPrintf(TEXT("sum(%d) = %d\n"),(int)param,sum);
}
__except(ExptFilter(GetExceptionInformation()))
{
if (GetExceptionCode() == EXCEPTION_STACK_OVERFLOW)
MessageBox(NULL,TEXT("堆栈溢出异常"),TEXT("异常"),MB_OK | MB_ICONWARNING);
else
MessageBox(NULL,TEXT("未预期的异常"),TEXT("异常"),MB_OK | MB_ICONWARNING);
}
}
__finally
{
MessageBox(NULL,TEXT("Finally 总是要执行的."),TEXT("Finally"),MB_OK | MB_ICONINFORMATION);
}
return 0;
}
//int _tmain(int argc,TCHAR *argv[])
int CALLBACK _tWinMain(HINSTANCE hPrev,HINSTANCE hInst,LPTSTR cmdLine,int show)
{
HANDLE hThread;
int n = 11800;
hThread = (HANDLE)_beginthreadex(0,0,ThreadProc,(void*)n,0,0);
if (hThread)
WaitForSingleObject(hThread,INFINITE);
CloseHandle(hThread);
return 0;
}
本程序的功能是计算从整数1加到整数n的和。程序使用递归和非递归两种方法来解决此问题:如果定义了USE_RECURSION宏,就使用递归解决方法,否则使用非递归解决方法。下面按照重要程度对程序的知识点进行一些总结。
1 递归解决方法中,当整数n太大时,函数递归调用层数过多,会引起堆栈溢出。程序用SEH(结构化异常处理)捕获此异常。相关代码是第105行左右的except子句。这是结构化异常处理的一般用法,没什么特别的。要注意的是,在异常过滤器函数ExptFilter()中,代码的第38处的MessageBox()函数调用是不正确的。因为在这里,线程已经堆栈溢出了,不能再嵌套地调用任何函数,因为函数调用是需要堆栈支持的(调用前保存返回地址到堆栈中,调用后从堆栈取出返回地址,并跳转到此地址)。然而,在异常处理代码(except子句)中,可以进行函数调用,因为此时系统已经确定要执行异常处理代码,发生异常的函数调用(103行对NSum的调用以及第86行NSum自身的多级递归调用)都已经全部退栈,线程已经从堆栈溢出状态中恢复。
2 非递归解决方法使用虚拟内存保存计算的中间结果,相当于用虚拟内存替换递归方法中的函数调用堆栈。
(1)代码第13和14行定义了非递归方法中用到的全局变量:g_pStack相当于递归调用时候的线程堆栈;g_nDepth是调用的深度。代码第100和101行对这两个变量进行初始化。
(2)代码第66至80行用非递归方法解决从1加到整数n的问题。当然,这样做是很无聊的,这个问题的解决本来是很简单的,这里是为了使用虚拟内存技术才故意这么写的。
(3)由于第100行在初始化g_pStack时,只预留地址空间,而没有提交物理存储器,所以第73行代码在执行时会引起拒绝访问异常。异常过滤器函数ExptFilter()第42行识别出这种异常,给出提示对话框。然后,第51行进行物理存储器的提交。
注意:
a 提交物理存储器时给出的起始地址是pExp->ExceptionRecord->ExceptionInformation[1],而不是pExp->ExceptionRecord->ExceptionAddress。要注意二者的区别:前者是发生拒绝访问异常时,企图访问的地址,即因为企图访问此地址才引起拒绝访问异常的。它的值仅在发生拒绝访问异常时有效。(目前只有拒绝访问异常定义了ExceptionInformation字段,其他类型异常的这个字段没有定义)。后者是引发异常的指令的地址,它对于任何类型的异常都是有效的。总的来说,前者是数据地址,只对拒绝访问异常有意义;后者是代码地址,对任何异常有意义。
b 提交的物理存储器大小是任意给出的29。其实,虚拟空间的预留和提交都是按页进行的,这个值29会自动转变成4KB(一页的大小),系统提交一页物理存储器。
c 提交物理存储器后,发生拒绝访问异常时企图访问的地址变为有效,程序可以访问此地址了,所以第54行处异常过滤器函数ExpFilter()返回EXCEPTION_CONTINUE_EXECUTION,让系统重新执行发生异常的指令。这次执行就不会引起拒绝访问异常了。
非递归方法避免了递归方法由于函数嵌套调用层数过深而引起的,不可恢复的堆栈溢出错误,在n值比较大时也可以正确第解决问题。因为线程的堆栈通常是很小的(默认为1MB),而线程可用的虚拟地址空间可接近2GB(32位Windows中,进程地址空间为4GB,其中约2GB被系统使用)。
当然,创建线程时可以指定线程堆栈大小(修改128行处_beginthreadex调用的第二个参数),但是对于函数的多级递归调用,我们无法准确了解至少要多大堆栈才够用(因为不了解系统函数调用使用堆栈的细节情况)。唯一的方法是指定尽量大的堆栈大小。
变通的方法是把递归的解决方案变成非递归的。不过,虽然从理论上来说,递归的算法都可以转换成非递归的,本程序涉及的问题的非递归解法也很容易实现,但是对于较复杂的问题,非递归解法却不是那么容易构造的。
本程序在使用虚拟地址空间时,采用先预留空间,在必要时才提交物理存储器的策略。这样可以避免不必要地占用系统页面文件空间的问题。本程序通过捕获到拒绝访问异常而判断需要提交物理存储器,还有一种方法是通过为页面设置PAGE_GUARD属性来实现,这个以后有机会再写程序做试验。
以上两点是本文的重点。下面再按照在程序中出现的次序,说说本程序可以总结的一些地方。
3 第17行的TStringPrintf()实现了(Unicode和非Unicode)通用的的printf()函数。C/C++标准库提供的printf()是不能处理Unicode字符串的。为了能处理Unicode,C/C++标准库引入了tchar.h头文件和wchar_t类型。但是,在Visual C++ 6.0中,在程序定义了_UNICODE宏时,使用_tprintf()和wprintf()都不能正确输出,不知道是Visual C++提供的C/C++标准库的bug,还是我的使用方法不对。
4 程序92行的ThreadProc()函数中使用了嵌套的try语句。Windows的结构化异常处理中,try语句只能跟随异常处理子句except或者结束处理子句finally。如果要同时使用except和finally子句,必须使用这种嵌套的形式。
5 第122行处main()和WinMain()函数的定义。Visual C++的编译器可以自动根据程序是否定义了UNICODE宏,以及是否定义了main(),_tmain(),WinMain,_tWinMain()函数来确定程序的类型:是否使用Unicode,是控制台程序还是一般的Windows程序。
相关文章推荐
- Windows网络编程入门:简单的客户端和服务器通信程序调试
- Windows网络编程入门:简单的客户端和服务器通信程序调试
- 最简单的Delphi程序(Windows)
- windows终止处理程序( __try __finally) 简单解析
- MFC入门——简单的Windows图形界面小程序
- MFC Windows 程序设计->WinMain 简单Windows程序 命令行编译
- Windows平台下简单运行Java程序的方法
- 一个简单的Windows下的socket程序
- 一个简单c语言windows程序的实现
- Windows网络编程入门:简单的客户端和服务器通信程序调试
- Linux程序代码移植到Windows的简单方法[转摘]
- 使用Delphi,SDK编写Windows简单程序
- 最简单的C# Windows服务程序
- 最简单的Windows窗口程序,使用main函数,隐藏控制台等,适合window编程入门
- python编写简单后门程序(支持windows和linux且不乱码)
- 一个简单的Windows下的socket程序
- windows简单窗口程序
- [windows编程-定时器]在控制台程序下,settimer不能简单了事
- Directx3D9学习之二:Windows编程之最简单窗口程序
- 【转】Windows下与Linux下编写socket程序的区别(简单区别,没有异步socket,如select)