用Windows 结构化异常处理及虚拟内存的简单程序

前段时间再次基本把《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,还是我的使用方法不对。

程序92行的ThreadProc()函数中使用了嵌套的try语句。Windows的结构化异常处理中,try语句只能跟随异常处理子句except或者结束处理子句finally。如果要同时使用except和finally子句,必须使用这种嵌套的形式。

5 第122行处main()和WinMain()函数的定义。Visual C++的编译器可以自动根据程序是否定义了UNICODE宏,以及是否定义了main(),_tmain(),WinMain,_tWinMain()函数来确定程序的类型:是否使用Unicode,是控制台程序还是一般的Windows程序。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值