首页 > 其他分享 >[转][翻译]深入理解Win32结构化异常处理(一)

[转][翻译]深入理解Win32结构化异常处理(一)

时间:2024-06-06 22:24:09浏览次数:23  
标签:结构化 翻译 EXCEPTION Win32 handler REGISTRATION DWORD 异常 SEH

     在所有的Win32操作系统提供的功能里,最常用但是描述最不全的(underdocument)恐怕就是结构化异常处理了(structured exception handling (SEH))。当你想到Win32的结构化异常处理,你会想到 _try, _finally, 和 _except这些东西,你可以从任何一本Win32的书中找到SEH的很好的描述,即使是Win32SDK也有一个非常完备的关于_try, _finally, 和 _except等的结构化异常处理的概述。既然有这么多关于关于SEH的书,为什么还说它描述不全呢,那是因为本质上讲Win32的结构化异常处理是操作系统提供的服务。所有你能找到的关于SEH的书都是描述一种包装了操作系统内部实现的特定编译器的运行时库。微软的操作系统或者编译器厂商定义_try, _finally, 和 _except等关键字用以表意相关的操作,其他的编译器厂商完全可以定义其他的关键字进行相同的表意。也就是说编译器级的SEH封装了操作系统原生的SEH,使得我们无法接触到原生SEH的细节。也不知道为什么,编译器级别的SEH就像是一个大秘密,Microsoft的Visual C++和Borland的Borland C++都没有提供它们SEH的最低层的代码。这篇文章中,我们从编译器提供的SEH(通过代码生成和运行时库提供)中剥离操作系统提供的SEH深入探究SEH最基本的概念。我会避免使用真正的C++异常处理(真正的C++异常处理使用catch()代替_except),实际上真正的异常处理的实现方式和本文讨论的非常相似(当然了真正的C++异常处理会有一些额外的复杂的东西,讨论这些东西会掩盖SEH的本质,故略去不讲)。

      当一个线程故障发生时,操作系统会提供一个机会告知错误信息。具体点说就是,当一个线程错误发生,操作系统会调用一个用户定义的回调函数,这个回调函数定义一些用户想要的操作,比如让蜂鸣器发声,或者播放一段.wav格式的提示音。不管这个回调函数干什么,它最后的操作是返回一个值告诉操作系统下一步要干什么。Win32的异常回调函数格式(来源于标准的Win32头文件EXCPT.h)如下:

 EXCEPTION_DISPOSITION __cdecl _except_handler(     struct _EXCEPTION_RECORD *ExceptionRecord,     void * EstablisherFrame,     struct _CONTEXT *ContextRecord,     void * DispatcherContext     );


这个异常回调函数的第一个参数是一个指向结构体EXCEPTION_RECORD的指针,这个结构体定义在WINNT.H里如下:

typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; DWORD ExceptionFlags; struct _EXCEPTION_RECORD *ExceptionRecord; PVOID ExceptionAddress; DWORD NumberParameters; DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; }  EXCEPTION_RECORD;

第一个参数ExceptionCode是一个由操作系统分配给异常的数值,在WINNT.H里用#define定义了一系列的由STATUS_为前缀的异常代码,比如STATUS_ACCESS_VIOLATION 的异常代码是 0xC0000005,我们可以从Windows NT DDK的头文件NTSTATUS.H中找到更加完备的异常代码。

第四个参数ExceptionAddress异常发生的地址。

其他的参数可以暂时忽略。

异常回调函数_except_handler的第二个参数是一个指向establisher frame结构体的指针,这是SEH中一个很重要的参数,但是现在暂时忽略。

第三个参数是一个指向结构体CONTEXT的指针,CONTEXT结构体定义在WINNT.H里,它代表了特定线程的注册值。当用在SEH时,CONTEXT就表示异常发生时的注册值。顺带说一句,这个CONTEXT结构体与GetThreadContext和SetThreadContext所使用的结构体是同一个。

第四个参数DispatcherContext也可以暂时忽略。

CONTEXT结构体:

typedef struct _CONTEXT {     DWORD ContextFlags;     DWORD   Dr0;     DWORD   Dr1;     DWORD   Dr2;     DWORD   Dr3;     DWORD   Dr6;     DWORD   Dr7;     FLOATING_SAVE_AREA FloatSave;     DWORD   SegGs;     DWORD   SegFs;     DWORD   SegEs;     DWORD   SegDs;     DWORD   Edi;     DWORD   Esi;     DWORD   Ebx;     DWORD   Edx;     DWORD   Ecx;     DWORD   Eax;     DWORD   Ebp;     DWORD   Eip;     DWORD   SegCs;     DWORD   EFlags;     DWORD   Esp;     DWORD   SegSs; } CONTEXT;

 

简单归结一下前边所说的:当一个异常发生时,一个回调函数就会被调用,这个回调函数有四个参数,其中三个是指向结构体的指针。_except_handler回调函数接收丰富的异常信息(比如什么类型的异常发生了,在哪发生的),通过这些信息,异常回调函数决定要做什么。

这里留有一个疑问,当异常发生时,操作系统怎么知道从哪调用这个回调函数呢。答案是EXCEPTION_REGISTRATION。我们唯一能找到EXCEPTION_REGISTRATION定义的地方是Visual C++的运行时库的EXSUP.INC。

EXCEPTION_REGISTRATION struc     prev    dd      ?     handler dd      ? _EXCEPTION_REGISTRATION ends

你也可以从WINNT.H的NT_TIB结构体的定义中看到一个被称为_EXCEPTION_REGISTRATION_RECORD的数据类型,但是我们找不到任何关于这个数据类型的定义信息,这也就是为什么说SEH是underdocumented未被文档化的。
让我们回到刚才的问题,操作系统怎么知道当异常发生时要从哪调用回调函数呢,EXCEPTION_REGISTRATION有两部分,第一部分暂时先忽略,第二部分handler是一个指向_except_ handler回调函数的指针。但是问题是操作系统怎么找到这个EXCEPTION_REGISTRATION呢?

为了回答这个问题,让我们重申一下:结构化异常处理工作在每个独立的线程里的,也就是说,每个线程都有自己的异常处理回调函数。thread information block(也叫TEB,或者TIB)是一个重要的Win32数据结构它存储了当前运行的线程的信息。TIB里的第一个DWORD就是一个指向该线程EXCEPTION_REGISTRATION结构体。在Intel的Win32平台上,FS注册表总是指向当前的TIB,也就是说在FS:[0]处你可以找到指向EXCEPTION_REGISTRATION的指针。

总结一下:当异常发生时,操作系统查找异常线程的TIB,从中取得指向EXCEPTION_REGISTRATION的指针,在EXCEPTION_REGISTRATION中可以找到指向异常回调函数 _except_handler的指针。

通过上述信息我写了一小程序简要描述一下系统级的结构化异常处理。

Figure 3   MYSEH.CPP //==================================================// MYSEH - Matt Pietrek 1997// Microsoft Systems Journal, January 1997// FILE: MYSEH.CPP// To compile: CL MYSEH.CPP//==================================================#define WIN32_LEAN_AND_MEAN#include <windows.h>#include <stdio.h>DWORD  scratch;EXCEPTION_DISPOSITION__cdecl_except_handler(    struct _EXCEPTION_RECORD *ExceptionRecord,    void * EstablisherFrame,    struct _CONTEXT *ContextRecord,    void * DispatcherContext ){    unsigned i;    // Indicate that we made it to our exception handler    printf( "Hello from an exception handler\n" );    // Change EAX in the context record so that it points to someplace    // where we can successfully write    ContextRecord->Eax = (DWORD)&scratch;    // Tell the OS to restart the faulting instruction    return ExceptionContinueExecution;}int main(){    DWORD handler = (DWORD)_except_handler;    __asm    {                           // Build EXCEPTION_REGISTRATION record:        push    handler         // Address of handler function        push    FS:[0]          // Address of previous handler        mov     FS:[0],ESP      // Install new EXECEPTION_REGISTRATION    }    __asm    {        mov     eax,0           // Zero out EAX        mov     [eax], 1        // Write to EAX to deliberately cause a fault    }    printf( "After writing!\n" );    __asm    {                           // Remove our EXECEPTION_REGISTRATION record        mov     eax,[ESP]       // Get pointer to previous record        mov     FS:[0], EAX     // Install previous record        add     esp, 8          // Clean our EXECEPTION_REGISTRATION off stack    }    return 0;}


Main函数里有三段inline的ASM代码块,第一段通过在("PUSH handler" 和"PUSH FS:[0]")在栈上生成了一个EXCEPTION_REGISTRATION结构体。PUSH FS:[0]保存了FS:[0]先前的值使之成为结构体的一部分,这样栈上就有一个8字节的EXCEPTION_REGISTRATION。下一条指令MOV FS:[0],ESP将当前线程信息块TIB的第一个DWORD放入新的EXCEPTION_REGISTRATION。

你有可能会奇怪为什么我在栈上建立EXCEPTION_REGISTRATION而不是采用全局变量,理由是当你使用编译器的_try/_except的语法,编译器也是在栈上建立EXCEPTION_REGISTRATION的,我只是简单展示一下当你使用_try/_except时编译器会如何做的一个简化的版本。
第二个_asm代码段主要用于产生一个异常,MOV EAX,0清零EAX寄存器,然后MOV [EAX],1把寄存器的值当成一个内存地址,把1赋值给内存地址为零的内存这样就会产生一个异常。
最后一个_asm代码段移除异常处理:将先前FS:[0]的值还原,然后将EXCEPTION_REGISTRATION弹出堆栈(ADD ESP,8)。

编译完成后执行,你会发现,当MOV [EAX],1执行,它会引起一个access violation违规访问异常,操作系统查看TIB的FS:[0],找到EXCEPTION_REGISTRATION的指针,在这个结构体里是一个指向_except_handler的指针,操作系统会将前边所述四个参数入栈,然后调用异常处理函数。

在异常函数里首先执行printf,然后异常处理函数会解决异常,也就是EAX寄存器指向了一个不能写入的地址0,解决方式是更改CONTEXT的EAX的值使他指向一块可以写入的内存地址(scratch的地址,scrath是为了简化程序说明问题故意引入的),最后的操作将ExceptionContinueExecution返回。
当操作系统看到ExceptionContinueExecution返回,这就意味着你已经解决了问题,故障指令会重新运行。也就是说异常处理函数改变EAX的值使之指向可写入内存,MOV EAX,1会再次执行,main函数正常执行完毕。

Reference:

http://www.microsoft.com/msj/0197/exception/exception.aspx

http://en.wikipedia.org/wiki/Win32_Thread_Information_Block

由于运行在CLR,C#的异常堆栈信息,异常处理显得没有那么神秘,毕竟CLR做为一个平台包办了这一切,但C++的异常处理怎么实现呢,一直对这个问题很感兴趣,以前去codeproject上看过一些帖子对结构化异常处理稍微了解过一点,但是理解的很肤浅。这几天关注C#5碰到一篇采访C#架构师Anders Hejlsberg的帖子无意间发现了关于SEH的这个链接,一读觉着深入浅出,挺有意思,随性翻译一下备忘,没有严格按照原文翻译,而且也没有翻译完,以后抽时间做完它。难免有疏漏,欢迎指正,如果有高手指教提供更完备的资料在下感激不尽了。


---------------------
作者:salomon
来源:CNBLOGS
原文:https://www.cnblogs.com/salomon/archive/2012/06/20/2556134.html
版权声明:本文为作者原创文章,转载请附上博文链接!
内容解析By:CSDN,CNBLOG博客文章一键转载插件

标签:结构化,翻译,EXCEPTION,Win32,handler,REGISTRATION,DWORD,异常,SEH
From: https://www.cnblogs.com/xiaomingerniu/p/18236161

相关文章

  • [转][翻译]深入理解Win32结构化异常处理(四)
     如果你已经走了这么远,不把整个过程讲完对你有点不公平。我已经讲了当异常发生时操作系统是如何调用用户定义的回调函数的。我也讲了这些回调的内部情况,以及编译器是如何使用它们来实现__try和__except的。我甚至还讲了当某个异常没有被处理时所发生的情况以及系统所做的扫尾工......
  • OCPP1.6 v2 之 ocpp-1.6 edition 2 (中文翻译)
    开放充电点协议1.6第二版最终版,2017年9月28日目录1.范围.................................................................................................42.术语和约定.............................................................................................
  • RFC 6455-websocket协议 -- 中文翻译
    英文文档的在源地址:RFC6455-TheWebSocketProtocol中文翻译如下:(未格式化整理)///互联网工程任务组(IETF)请求评论:6455作者:I.Fette公司:Google,Inc.类别:标准轨道ISSN:2070-1721A.Melnikov公司:IsodeLtd.2011年12月WebSocket协议摘要WebSocket协议允许在受......
  • ARP协议:网络世界的地址翻译官
    一.引言  在当今快速发展的汽车行业中,车载以太网正逐步成为推动汽车智能化、网联化浪潮的核心技术之一。作为传统以太网技术在汽车领域的创新应用,车载以太网不仅继承了以太网的开放性、成熟性和互操作性,还针对车辆特有的环境和需求进行了优化与定制,为车载内部的复杂数据传......
  • Navi日语社App一款支持日文OCR文字识别提取的应用,功能丰富,支持日语翻译、语音翻译、日
    如果你正在寻找一款简单好用、功能丰富的日文OCR识别软件,那么推荐你试试《Navi日语社》App,在安卓和苹果手机上,很多应用都支持免费的日语翻译功能,但是支持日文OCR文字识别的软件并不多,针对这一痛点,准橙翻译开发上线了《Navi日语社》App,一款支持日文OCR识别提取文字的移动软件,识......
  • 老挝语翻译通App中国人出门在外都在用的老挝语翻译工具,支持老挝文OCR识别、文字转语音
    老挝语翻译通App,一款更加符合中国人用语习惯的翻译工具,在国内外都能正常使用的翻译器。当大家选择去东南亚国家旅游、GAP的时候,老挝这个国家是值得一去的,可以让大家感受到另一番风情。但是,在去之前,需要做一些准备:衣食住行都要提前规划和准备好,而贯穿整个旅途的另一个容易被......
  • QNX-9—QNX官网文档翻译—中断-3—Writing an Interrupt Handler
    翻译:QNXSoftwareDevelopmentPlatform-->Programming-->Programmer'sGuidehttps://www.qnx.com/developers/docs/7.1/index.html#com.qnx.doc.neutrino.prog/topic/inthandler.html前言:及时处理硬件事件的关键是硬件产生中断。中断只是处理器正在执行的操作的暂停或中断......
  • 关于结构化分析方法(状态图、ER图)
    课堂作业1.尝试建模电梯的状态图(选)2.学校规定:一个学生可选修多门课,一门课有若干学生选修;一个教师可讲授多门课,一门课只有一个教师讲授;一个学生选修一门课,仅有一个成绩。学生的属性有学号、学生姓名;教师的属性有教师编号,教师姓名;课程的属性有课程号、课程名。要求:根据上述语义......
  • 翻译《The Old New Thing》- What a drag: Dragging a Uniform Resource Locator (URL
    Whatadrag:DraggingaUniformResourceLocator(URL)andtext-TheOldNewThinghttps://devblogs.microsoft.com/oldnewthing/20080313-00/?p=23123RaymondChen 2008年03月13日 麻烦的拖拽:拖拽统一资源定位符(URL)和文本简要        这篇文章主要讲......
  • 翻译《The Old New Thing》- What a drag: Dragging a Uniform Resource Locator (URL
    Whatadrag:DraggingaUniformResourceLocator(URL)-TheOldNewThing(microsoft.com)https://devblogs.microsoft.com/oldnewthing/20080312-00/?p=23133RaymondChen 2008年03月12日麻烦的拖拽:拖拽统一资源定位符(URL)简要本文介绍了如何在Windows程序中实......