前段时间再次基本把《Windows核心编程》看完了一次(第一次是看的电子版的,这次是印刷版的),对书中描述的Windows系统虚拟内存管理和结构化异常处理的印象比较深。那时候工作上也没什么事情,于是就写了个小程序来练手。今天偶尔把它翻出来了,就改了改,作为今天的一篇技术方面的日志发表了吧。
#define
#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
#endif
static void TStringPrintf(const TCHAR *format,...)
{
}
static long ExptFilter(EXCEPTION_POINTERS* pExp)
{
}
static int NSum(int n)
{
#ifndef USE_RECURSION
#else
#endif
}
static DWORD CALLBACK ThreadProc(LPVOID param)
{
#ifndef USE_RECURSION
#endif
}
//int _tmain(int argc,TCHAR *argv[])
int CALLBACK _tWinMain(HINSTANCE hPrev,HINSTANCE hInst,LPTSTR cmdLine,int show)
{
}
本程序的功能是计算从整数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
5 第122行处main()和WinMain()函数的定义。Visual C++的编译器可以自动根据程序是否定义了UNICODE宏,以及是否定义了main(),_tmain(),WinMain,_tWinMain()函数来确定程序的类型:是否使用Unicode,是控制台程序还是一般的Windows程序。