天天看点

windows用户态程序排错——异常处理

关于异常

1、C++标准异常:

c++提供了一种异常的处理方法:try()catch();

不知道小伙伴有没有这样的疑惑:1)为何我们要捕获异常?2)何时使用异常处理?3)异常处理会不会降低程序性能?4)如何处理异常呢?

1)为何需要捕获异常?何时抛出异常?

异常处理就是处理程序中的错误。

异常,通俗来说,对于被调用的函数,如果说因为调用者所引发的错误,且无法继续执行时,需要通知调用者,发生错误无法执行了;让上层的逻辑去处理它。

异常处理,对于程序来说,可以知道什么出错了?哪里出错了?以及为什么出错了?这可以作为程序的一大调试手段。

而且,作为一个底层函数,出了错误,如果需要return,则需要定义好一系列的错误类型,让上层的调用程序知道具体的错误;当存在大量的if else时,这些错误处理代码与正常的业务代码就会纠缠在一起,为了编译开发维护,此时你就该使用try catch处理异常了。

异常处理从另一个角度看,需要调用者自己处理可能发生的错误,比如说除数为0,就是需要调用者自己排除才对,如果通过if语句检查,并return 错误码,如果调用者没有检查返回值呢?所以使用异常处理,可以强制方法调用值处理可能发生的错误。

2)何时使用异常处理呢?

这里引用C++之父的话,一个库的作者可以检测出发生了运行时错误,但一般不知道怎么处理它们。而异常的基本目的就是处理这些错误,对自身无法处理的错误,抛出一个异常,让它的调用者能够处理这个问题。

从前面的描述,大致可以认为:对于可预测的异常情况,像被除数为0是可以用if、else的进行处理,使用exception是为了,让if/else 纯粹的去操作业务逻辑 ,不会应为过多的逻辑分支影响代码的可读性和可维护;

对于不可预测的异常情况,比如读取文件,序列化对象,面对种种可能引发异常的情况。此时exception 提供了统一的处理方式。

3)异常处理会不会降低性能?

那么异常处理会不会减低性能呢?从有关资料的测试中来看,C++的 try部分代码运行并不会有明显的性能降低问题;当出现异常,catch异常是会消耗一点点时间的,但在正常运行时,并没有造成什么性能上的损耗。

4)如何处理异常?

如何处理异常,即异常的基本语法和使用注意项:

<1>首先是抛出与捕获异常:

抛出使用throw,捕获使用try.....catch,举例如下:

https://blog.csdn.net/daheiantian/article/details/6530318?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param

具体这里不再赘述。

2. Win32系统提供的SEH(结构化异常处理)

所谓的结构化异常,通俗来说就是当程序出现错误时,操作系统会调用用户定义的一个回调函数:

EXCEPTIO_DISPOSITION

__cdecl _except_handler( struct _EXPECTION_RECORD *ExceptionRecord, void * EstablisherFrame, sturct _CONTEXT *ContextRecord, void *DispatcherContext);

值得注意的是,此种方法未windows提供,并不能夸平台。

语法介绍:

1)try-except语句:

try-except语句可以使被保护的代码出现异常时候进行控制,其中__except表达式括号内的值可以有以下三种值:

EXCEPTION_CONTINUE_EXECUTION(-1)异常被忽略,在发生异常的位置继续执行。

EXCEPTION_CONTINUE_SEARCH(0)无法处理异常。继续在调用链中寻找能够处理异常的代码。

EXCEPTION_EXECUTE_HANDLER(1)使用__except块里的代码处理异常,然后在__except块后继续执行。

__try{
    // guarded code
}
__except ( expression ){
    // exception handler code
}
           

2)try-finally语句

在try-finally语句中,不管__try块是正常结束还是异常结束,__finally块的代码总是会被执行。但在__try块中发生异常时,需要有异常处理程序能否处理这个异常,例如外部嵌套的try-except语句,否则程序将会终止,try-finally语句并不会处理异常。当外部有except异常处理程序,则会优先执行__finally块,再执行异常处理程序。

in

t main(){
    __try
    {
        __try
        {
            RaiseException(0xc0000005, 0, 0, 0);
        }
        __finally
        {
            cout << "__finally" << endl;
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        cout << "__except"<< endl;
    }
    system("pause");
}
           

此语句有个非常好的用法就是:在__try块中分配资源,在__finally块中判断资源不为空时释放它,这样可以保证所有资源都能被安全的释放。

3)__leave语句

由于在__try块中使用return、goto、continue、break退出__try块时会产生额外的开销,这里我们可以使用__leave退出__try块,以避免不必要的开销。

这里还需注意异常相关的编译选项:

/EHs /EHsc(默认选项)

try-catch只处理C++异常,不捕捉结构化异常

try-except处理C++异常和结构化异常

在使用try-except捕捉到结构化异常时,try块内的局部对象不会被自动析构

/EHa

try-catch捕捉C++异常和结构化异常

try-except处理C++异常和结构化异常

在使用try-except捕捉到结构化异常时,try块内的局部对象会自动析构

无/EH

try catch不处理异常

try-except处理C++异常和结构化异常

在使用try-except捕捉到结构化异常时,try块内的局部对象不会被自动析构

常用的一些API:

GetExceptionCode,获取异常代码,只能在异常过滤表达式或异常处理块中调用。

GetExceptionInformation,获取异常相关信息,只能在异常过滤表达式中使用,返回的数据保存在转移到异常处理块中时不再可用。

RaiseException,触发一个异常,自定义异常代码最好以0xE开头(例如0xE0000001),以避免和Windows异常代码冲突。

AbnormalTermination,如果__try块正常结束则返回0(包括__leave导致的结束),否则返回非0(包括return,goto,continue,break导致的结束),只能在__finally块中调用该函数。

3. 异常时保存关键信息minidump

相信使用windows的小伙伴都接触过windows蓝屏问题,windows蓝屏时,如果系统设置了内存转储,则会将奔溃时的内存数据存储在文件中,用于后续的蓝屏问题分析排错。

这种内存转储文件,报错了故障时所有程序的数据与状态,我们可以通过windbg分析文件,分析排查出具体哪个应用、驱动等导致的操作系统奔溃问题。

这种转储文件一般都是非常多,如果是完全内存转储,一般会生成和PC的内存一样的转储文件,文件一般存储在C:\\windows目录下;因此如果操作系统的C盘空间不足,也会导致生成转储文件失败。

那如果我们的应用程序奔溃时,是否也可以生成奔溃转储文件呢?

自Windows XP以来,微软提供了一种”minidump”的奔溃转储技术,它保存了故障进程的所有线程的调用堆栈,以及局部变量的值,这种dump文件相比于系统的转储文件占用很少的字节,通常只有几K字节。通过触发这种奔溃转储,开发人员可以尽可能的获取奔溃发生时,程序运行的各种信息。

那如何在自己的程序中集成minidump,以便程序异常是,可以将异常时刻的故障信息保存下来,方便后续的排查?

好在已经有现成的函数API供我们使用了,我们可以通过DbgHelp函数组提供的函数来创建自己程序的minidump:

1)MiniDumpWriteDump函数

BOOL MiniDumpWriteDump(

HANDLE hProcess, //进程句柄

DWORD ProcessId, //进程ID

  HANDLE hFile, //创建的文件句柄

  MINIDUMP_TYPE DumpType, //MINIDUMP_TYPE

  PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,

  PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,

  PMINIDUMP_CALLBACK_INFORMATION CallbackParam

);
           

minidump中应该包含哪些数据?主要就是取决于DumpType变量。

常用的DumpType值:

MiniDumpNormal:值为0,表示一组基础的数据集合。

MiniDumpWithFullMemory:值为2, 表示包含进程地址空间素有可读页面的内容。

这样我们可以实现一个基础的写minidump方法:

void CreateMiniDump( EXCEPTION_POINTERS* pep )
{
  // Open the file
  HANDLE hFile = CreateFile( _T("MiniDump.dmp"), GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL );
  if( ( hFile != NULL ) && ( hFile != INVALID_HANDLE_VALUE ) )//创建文件
  {
    // Create the minidump
    MINIDUMP_EXCEPTION_INFORMATION mdei;
    mdei.ThreadId = GetCurrentThreadId();
    mdei.ExceptionPointers = pep;//指向故障信息
    mdei.ClientPointers = FALSE;
    MINIDUMP_TYPE mdt = MiniDumpNormal;//指定minidump收集的信息
    BOOL rv = MiniDumpWriteDump( GetCurrentProcess(), GetCurrentProcessId(), hFile, mdt, (pep != 0) ? &mdei : 0, 0, 0 );
    if( !rv )
      _tprintf( _T("MiniDumpWriteDump failed. Error: %u \n"), GetLastError() );
    else
      _tprintf( _T("Minidump created.\n") );
    // Close the file
    CloseHandle( hFile );
  }
  else
  {
    _tprintf( _T("CreateFile failed. Error: %u \n"), GetLastError() );
  }
}
           

2)MiniDumpCallback函数

如果MINIDUMP_TYPE不能满足我们定制minidump内容的需要,我们可以使用MiniDumpCallback函数。这是一个用户定义的回调函数,MiniDumpWriteDump会调用它,让用户来决定是否把某些数据放到minidump中。通过这个函数,我们可以完成这些功能。

具体可见:https://www.cnblogs.com/lidabo/p/3635960.html

基于上述API,我们就可以在自己的代码中集成minidump了:

可以使用Windows的SEH的API:SetUnhandledExceptionFilter,设置一个异常过滤器,这样每当一个SEH发生时,如果系统找不到合适的处理代码,就会调用设置的过滤函数,进行处理。

在我们设置的异常处理函数中,我们就可以使用上面的minidump,保存当前程序的dump文件。

继续阅读