《Windows API每日一练》22.3 SHE异常

本节我们将讲述单线程到多线程的演进过程,以及进程与线程的区别。

本节必须掌握的知识点:

        SHE异常

        第170练:SEH异常处理程序

        第171练:setjmp和longjmp进行异常捕获与处理

22.3.1 SHE异常

在C语言中,Windows平台提供了结构化异常处理(Structured Exception Handling,SEH)机制,用于捕获和处理异常。SEH允许程序在发生异常时执行特定的异常处理代码,以实现异常安全和错误处理。

SEH使用以下两个主要的关键字/函数来实现异常处理:

__try:__try关键字用于标记一段代码块,该代码块可能会引发异常。在__try块中,可以包含可能引发异常的代码。

__except:__except关键字用于指定异常处理代码块,用于处理发生在__try块中的异常。在__except块中,可以编写处理异常的代码逻辑。

以下是使用SEH进行异常处理的基本结构:

__try {

    // 可能引发异常的代码块

    // ...

}

__except (ExceptionFilterFunction(GetExceptionCode(), GetExceptionInformation())) {

    // 异常处理代码块

    // ...

}

在上述代码结构中,__try块中的代码可能会引发异常。如果发生异常,控制流将转移到与__try块关联的__except块。__except块中的代码将处理异常,并根据需要执行适当的操作。

在__except块中,可以使用ExceptionFilterFunction来指定异常过滤函数,它接受异常代码和异常信息作为参数,并返回一个值用于指示如何处理异常。可以根据异常类型和其他条件来决定如何处理异常。

除了__try和__except之外,SEH还提供其他一些关键字和函数,如__finally和__leave,用于执行清理操作或控制控制流。

下面是一个简单的示例,演示了如何使用SEH处理除以零异常:

#include <stdio.h>

#include <windows.h>

int main() {

    __try {

        int dividend = 10;

        int divisor = 0;

        int result = dividend / divisor;  // 可能引发除以零异常

        printf("Result: %d\n", result);

    }

    __except (EXCEPTION_EXECUTE_HANDLER) {

        printf("Exception: Division by zero\n");

    }

    return 0;

}

【注意】SEH是特定于Windows平台的机制,在其他操作系统或编译器上可能没有直接的等效实现。此外,SEH处理的异常类型是系统定义的异常,如访问冲突、除以零等。对于C语言中的其他类型异常,可以使用C++异常处理机制(try-catch块)或其他库提供的异常处理机制。

22.3.2 第170练:SEH异常处理程序

/*------------------------------------------------------------------------

 170 WIN32 API 每日一练

     第170个例子seh01.c:SEH异常

(c) www.bcdaren.com 编程达人

-----------------------------------------------------------------------*/

#include <windows.h>

#include <stdio.h>

DWORD scratch;

//异常回调函数

int CALLBACK _Handler(PEXCEPTION_RECORD lpExceptionRecord, DWORD lpSEH,

    PCONTEXT lpContext,PVOID lpDispatcherContext)

{

    const TCHAR szMsg[] = TEXT("异常发生位置:%08X,异常代码:%08X,

标志:%08X");

    static TCHAR szBuffer[256];

    PCONTEXT pContext;

    PEXCEPTION_RECORD pException;

    pContext = lpContext;

    pException = lpExceptionRecord;

    wsprintf(szBuffer, szMsg, pContext->Eip, pException->ExceptionCode,

pException->ExceptionFlags);

    MessageBox(NULL, szBuffer, NULL, MB_OK);

    pContext->Eax = (DWORD)&scratch;

    //goto _SafePlace;

    //return EXCEPTION_EXECUTE_HANDLER;//结束程序

//异常不被识别,系统将继续到上一层的try-except域中继续查找

//一个恰当的__except模块

    //return EXCEPTION_CONTINUE_SEARCH;

    //return EXCEPTION_CONTINUE_EXECUTION;//错误已经被修复,从异常发生处继续执行

    /*注意:EXCEPTION_CONTINUE_EXECUTION将导致死循环,因为mov dword ptr[eax], 0的机器指令依然存在,并未被修改,恢复保护栈,并使用汇编指令修改EIP地址,才可以继续执行。*/

    return ExceptionContinueExecution;//等于0,继续执行

    //return ExceptionContinueSearch;//等于1,交给下一个节点处理

    //return ExceptionNestedException;//等于2,发生嵌套异常

    //return ExceptionCollidedUnwind;//等于3,发生了嵌套展开操作

}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,

    LPSTR lpCmdLine, int nShowCmd)

{

    TCHAR buff[256];

    const TCHAR szCaption[] = TEXT("SEH例子");

    //在堆栈中构造一个 EXCEPTION_REGISTRATION 结构

    _asm

    {

        //将回调函数的地址推入堆栈

        push offset _Handler

        //线程信息块NT_TIB结构偏移地址fs:[0]处为第一个字段 ExceptionList

        //指向一个 EXCEPTION_REGISTRATION 结构

        //将原先使用的EXCEPTION_ REGISTRATION结构地址推入堆栈

        push fs:[0]

        //[esp]等于原结构地址prev字段,而[esp + 4] 等于回调函数地址handler字段

        mov fs:[0],esp

        //会引发异常的指令

        xor eax,eax

        mov dword ptr[eax], 1234h   //产生异常,然后_Handler被调用

    //以下指令将不会被执行

    //...

    //异常处理后跳转地址

//_SafePlace:

    }

    wsprintf(buff, TEXT("写入数据完毕,scratch=0x%x!\n"), scratch);

    MessageBox(NULL, buff, szCaption, MB_OK);

    //恢复原来的 SEH 链

    _asm

    {

        //从堆桟中的prev字段中弹出原来的fs:[0]值

        pop fs:[0]

        //堆栈平衡, 没有实际用途

        pop eax

    }

    ExitProcess(0);

    return 0;

}

运行结果:

图22-2 SHE异常处理

 

总结

       实例使用内联汇编安装了一个SHE异常,并向0地址处写入数据,引发一个异常。发生异常时,由异常回调函数接管异常,处理异常后继续执行异常发生后的下一条指令。

●回调函数的参数

SEH异常处理回调函数的参数定义与筛选器回调函数的参数定义有所不同,其定义如下:

//异常回调函数

int CALLBACK _Handler(PEXCEPTION_RECORD lpExceptionRecord, DWORD lpSEH,

        PCONTEXT lpContext,PVOID lpDispatcherContext)

在这个回调函数中,前面的3个参数是要用到的。其中的_lpExceptionRecord参数指向一个EXCEPTION_RECORD结构;_lpContext参数指向一个CONTEXT结构,当用于SEH 时,CONTEXT 结构体保存着发生异常时各寄存器的值。这两个结构提供的数据就相当于上一节中筛选器回调函数从参数中得到的数据,可以用同样的方法来使用它们;_lpSEH参数指向注册回调函数时使用的EXCEPTION_REGISTRATION结构的地址,在例子程序中,它的值就是我们在堆栈中构造的这个结构的地址,这个参数 看上去似乎没有什么用处,例子程序中也确实没有用到它,但是如果希望异常处理程序能 够被封装在子程序里面的话,这个参数就是不可缺少的,因为使用它可以避免使用全局变量在模块和回调函数之间传递数据,在接下来的内容中读者会了解到如何做到这一点。

●回调函数的返回值

SEH异常处理回调函数的返回值定义不同于筛选器异常处理回调函数,它可以使用下面列出的4种取值。

1.ExceptionContinueExecution (等于0):回调函数返回后,系统将线程环境设置为 _lpContext参数指定的CONTEXT结构并继续执行。

2.ExceptionContinueSearch (等于1):回调函数拒绝处理这个异常,系统将通过 EXCEPTION_REGISTRATION结构的prev字段得到前一个回调函数的地址并调用它。

3.ExceptionNestedException (等于2):回调函数在执行中又发生了新的异常,即发生了嵌套的异常。

4.ExceptionColIidedUnwind (等于3):发生了嵌套的展开操作(展开操作的介绍请读者参考MSDN的解释,此处不再展开)。

● SEH链和异常的传递

每次定义了一个新的SEH异常处理回调函数时,EXCEPTION_REGISTRATION结构 的prev字段都被要求填写为原来的EXCEPTION_REGISTRATION结构地址,随着应用程序对执行模块的调用一层层深入下去,如果有多个模块设置了回调函数,那么到最后全部 的回调函数会形成一个SEH链,如图22-3所示。

当程序中有多个线程在运行的时候,每个线程中都会存在各自的SEH链,这些SEH链中指定了多个回调函数,除它们以外,系统中可能还会存在一个全局性的筛选器异常处理回调函数,再者,如果进程被调试的话,调试器进程也相当于一个异常处理程序存在。 既然会同时存在这么多的回调函数,而每个函数都可能对发生的异常提出不同的处理意见,那么当一个异常发生的时候,系统究竟该听谁的意见呢?

图22-3 异常处理链

在这种情况下,系统按照一定的步骤选择一个回调函数并执行它,如果这个被执行的回调函数可以处理这个异常,那么程序被修正后继续执行并且其他的回调函数不会再被执行,否则系统继续执行下一个回调函数,查找的步骤如下:

1.系统查看产生异常的进程是否正在被调试,如果正在被调试的话,那么向调试器发送 EXCEPTION_DEBUG_EVENT 事件。

2.如果进程没有被调试或者调试器不去处理这个异常,那么系统检查异常所处的线程,并在这个线程的环境中查看fs:[0]来确定是否安装有SEH异常处理回调函数,如果 有的话则调用它。

3.回调函数尝试处理这个异常,如果可以正确处理的话,则修正错误并将返回值设置为ExceptionContinueExecution,这时系统将结束整个查找过程。

4.如果回调函数返回ExceptionContinueSearch,告知系统它无法处理这个异常,那么系统将根据SEH链中的prev字段得到上一个回调函数地址并重复步骤(3),直到链中的某个回调函数返回ExceptionContinueExecution为止,查找结束。

5.如果到了SEH链的尾部却没有一个回调函数愿意处理这个异常,那么系统将再次检测进程是否正在被调试,如果被调试的话,则再一次通知调试器。

6.如果调试器还是不去处理这个异常或者进程没有被调试,那么系统检查有没有安装筛选器回调函数,如果有,则去调用它,筛选器回调函数返回时,系统默认的异常处理程序根据这个返回值将做相应的动作。

7.如果没有安装筛选器回调函数,系统直接调用默认的异常处理程序终止进程。

这个过程归纳起来就是:系统按照调试器、SEH链上从新到旧的各个回调函数、筛选器回调函数的步骤一个个去调用它们,一直到某个回调函数愿意处理异常为止。如果大家都无法处理异常的话,那么最后由系统默认的异常处理程序来终止发生异常的进程。

22.3.3 第171练:setjmp和longjmp进行异常捕获与处理

/*------------------------------------------------------------------------

 171 WIN32 API 每日一练

     第171个例子seh02.c:使用setjmp和longjmp进行异常捕获与处理

     setjmp函数

     longjmp函数

 (c) www.bcdaren.com 编程达人

-----------------------------------------------------------------------*/

#include <windows.h>

#include <setjmp.h>

jmp_buf j;

//异常回调函数

int CALLBACK _Handler()

{

    const TCHAR szMsg[] = TEXT("异常发生");

    MessageBox(NULL, szMsg, NULL, MB_OK);

    //从jmp_buf结构中恢复setjmp保存的上下文,该函数不返回,而是从setjmp中返回

    //参数setjmpj为保存过的上下文,参数1为返回值

    longjmp(j,1);

    return EXCEPTION_CONTINUE_EXECUTION;//错误已经被修复,请从异常发生处继续执行

    //return EXCEPTION_EXECUTE_HANDLER;//结束程序

    //return EXCEPTION_CONTINUE_SEARCH;//查找下一个异常处理模块

}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,

    LPSTR lpCmdLine, int nShowCmd)

{

    const TCHAR szSafe[] = TEXT("回到了安全的地方!");

    const TCHAR szCaption[] = TEXT("SEH例子");

    //将此处的上下文保存在jmp_buf结构中,如果调用时返回值为0

    //如果从longjmp调用返回,返回值为非0

    if (setjmp(j) == 0)//第一次调用返回值为0

    {

        if (hInstance != NULL)

            _Handler();

    }

    else

    {

        MessageBox(NULL, szSafe, szCaption, MB_OK);

    }

    ExitProcess(0);

    return 0;

}

       运行结果:

图22-4 长跳转

 

总结

       实例seh02.c演示了使用setjmp和longjmp进行异常捕获与处理。与实例seh01.c的不同在于,可以实现在不同函数内,地址标号的长跳转。实例在调用longjmp函数后,控制流跳转回了WinMain函数,并且从跳转点之后的代码开始执行。

       setjmp和longjmp是C语言中的函数,用于实现非局部的跳转,通常用于异常处理或实现协程(coroutine)等。

setjmp函数在一个程序中设置一个跳转点,以便稍后可以使用longjmp函数从任何位置跳转回该点。setjmp函数的原型如下:

#include <setjmp.h>

int setjmp(jmp_buf env);

setjmp函数接受一个jmp_buf类型的参数 env,它是一个用于保存跳转点信息的缓冲区。jmp_buf类型实际上是一个数组,用于保存寄存器状态、栈指针和其他相关信息,以便在跳转回来时能够恢复到相应的状态。

setjmp函数返回一个整数值,它用于确定函数是直接返回还是从longjmp调用中返回。如果setjmp直接返回,则返回值为0;如果从longjmp调用中返回,则返回非零的值。

【注意】setjmp和longjmp的使用需要谨慎,因为它们会绕过正常的函数调用和返回机制,可能导致程序逻辑混乱和难以调试的问题。在使用时,必须确保跳转点的上下文能够正确地恢复,以避免未定义的行为。此外,跳转点应该在相同的函数调用栈内才能正常工作,否则会导致未定义的行为。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/51819.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

定制数据流:在Mojo模型中打造个性化数据预处理

定制数据流&#xff1a;在Mojo模型中打造个性化数据预处理 数据预处理是机器学习工作流程中的关键步骤&#xff0c;它直接影响到模型的性能和训练效率。Mojo模型&#xff0c;作为一个先进的机器学习框架&#xff0c;提供了强大的扩展性来支持自定义数据预处理。本文将深入探讨…

代码随想录算法训练营Day54|| 图论part04

图论部分就先不手写代码了。能理解就很花时间了&#xff0c;先看懂逻辑和代码&#xff0c;关键基础部分写写吧。 卡玛网110字符串接龙&#xff1a;相当于求无向图的最短路径&#xff0c;广搜最合适&#xff0c;因为广搜第一次找到路径一定最短。 广搜就要利用队列&#xff0c;代…

JavaScript while 循环

JavaScript while 循环 JavaScript 中的 while 循环是一种基本的循环控制结构&#xff0c;它重复执行一段代码&#xff0c;直到指定的条件不再满足为止。这种循环结构在处理不确定次数的循环时非常有用&#xff0c;尤其是当循环的次数依赖于某些运行时条件时。 基本语法 whi…

一款免费开源的AI贴纸生成工具

StickerBaker是一款免费开源的AI贴纸生成工具&#xff0c;旨在通过简单的文本输入快速创建个性化贴纸。用户只需在输入框中输入关键词或短语&#xff0c;如“猫”、“击掌”等&#xff0c;AI就会将这些文本转换为相应的图像贴纸。该工具支持批量生成&#xff0c;可以一次性输入…

C语言中的特殊指针

文章目录 &#x1f34a;自我介绍&#x1f34a;野指针&#x1f34a;void *指针&#x1f34a;NULL指针 你的点赞评论就是对博主最大的鼓励 当然喜欢的小伙伴可以&#xff1a;点赞关注评论收藏&#xff08;一键四连&#xff09;哦~ &#x1f34a;自我介绍 Hello,大家好&#xff0c…

苹果iPhone手机将使用QLC NAND闪存技术,存储或将提升

本文首发于公众号“AntDream”&#xff0c;欢迎微信搜索“AntDream”或扫描文章底部二维码关注&#xff0c;和我一起每天进步一点点 苹果公司计划在未来的iPhone产品中使用QLC NAND闪存技术&#xff0c;这一技术将对iPhone的存储性能带来显著提升。以下是一些关键点&#xff1a…

SRv6 和IGP/BGP协议区别

目录 SRv6 和IGP/BGP协议区别 SRv6 IGP/BGP SRv6与IGP/BGP的区别 SRv6 和IGP/BGP协议区别 SRv6(Segment Routing over IPv6)和IGP(Interior Gateway Protocol,内部网关协议)/BGP(Border Gateway Protocol,边界网关协议)在网络架构和功能上存在显著差异。下面分别…

中南民族大学学报人文社会科学版

《中南民族大学学报&#xff08;人文社会科学版&#xff09;》是由国家民委主管、中南民族大学主办的综合性学术理论研究期刊。1960年创刊&#xff0c;全面刊载民族学、人类学、社会学、哲学、政治学、法学、经济学、文学、历史学等各学科优秀科研成果&#xff0c;是展示国内人…

鸿蒙应用框架开发【简单时钟】 UI框架

简单时钟 介绍 本示例通过使用ohos.display接口以及Canvas组件来实现一个简单的时钟应用。 效果预览 使用说明 1.界面通过setInterval实现周期性实时刷新时间&#xff0c;使用Canvas绘制时钟&#xff0c;指针旋转角度通过计算得出。 例如&#xff1a;"2 * Math.PI / …

视频剪辑常用工具

视频剪辑 1. Adobe Premiere Pro (PR)2. Final Cut Pro X (FCPX)3. DaVinci Resolve4. 剪映5. FFmpeg一、安装FFmpeg二、基本剪辑操作1. 裁剪视频2. 合并视频3. 转换视频格式 三、高级剪辑操作1. 添加水印2. 提取音频 四、总结 视频剪辑常用软件及其信息 1. Adobe Premiere Pr…

Postman 工具网站测试工具 (用于发送请求测试处理业务逻辑)

今天在写go web开发的时候&#xff0c;学会了用postman 这个工具很方便的发送了一个post请求&#xff0c;其中body包含 JSON 格式的 web 服务收到请求回复 success 及打印

“微软蓝屏”“隐形炸弹”

“微软蓝屏”事件暴露了网络安全哪些问题&#xff1f; 近日&#xff0c;一次由微软视窗系统软件更新引发的全球性“微软蓝屏”事件&#xff0c;不仅成为科技领域的热点新闻&#xff0c;更是一次对全球IT基础设施韧性与安全性的深刻检验。这次事件&#xff0c;源于美国电脑安全技…

Linux操作系统内核升级及回退全过程

文章目录 **内核版本升级**1. **查看当前机器操作系统IP&#xff0c;操作系统版本和内核版本**2. **沟通至业务侧确认业务ip&#xff0c;确认应用下线&#xff0c;业务侧回复后开始操作**3. **建立本地yum源&#xff0c;挂载高版本内核镜像**4. **移除原有yum源&#xff0c;新建…

手写 Hibernate ORM 框架 00-环境准备

手写框架 类似的还有其他系列&#xff0c;主要用于学习其中的原理。 Hibernate 属于比较早的框架了&#xff0c;后期将实现一套 mybatis。 手写 Hibernate 系列 手写 Hibernate ORM 框架 00-hibernate 简介 手写 Hibernate ORM 框架 00-环境准备 手写 Hibernate ORM 框架…

Lc62---3024.三角形类型(排序)--java版

1.题目 2.思路 就是用选择结构进行if-else if的编写。 除了题目的条件&#xff0c;还要判断是不是三角形&#xff08;两边之和小于等于第三边&#xff0c;也无法构成三角形&#xff09; 3.代码实现 class Solution {public String triangleType(int[] nums) {if(nums.length…

命令行使用ADB,不用root,完美卸载小米预装软件

ADB安装与运行 install java 下载安装 注意选择JDK17以上版本 https://www.oracle.com/java/technologies/downloads/#jdk22-windows 选择中间的安装文件下载 编辑系统变量 C:\Program Files (x86)\Java\jdk-22 C:\Program Files (x86)\Java\jdk-22\bin 把C:\Progra…

nginx 启动 ssl 模块

文章目录 前言nginx 启动 ssl 模块1. 下载2. 启动 ssl 模块 步骤3. 验证前言 如果您觉得有用的话,记得给博主点个赞,评论,收藏一键三连啊,写作不易啊^ _ ^。   而且听说点赞的人每天的运气都不会太差,实在白嫖的话,那欢迎常来啊!!! nginx 启动 ssl 模块 1. 下载 下载…

Scrapy 爬取旅游景点相关数据(五)

本期内容&#xff1a;&#xff08;1&#xff09;爬取日本其他城市数据存入数据库&#xff08;2&#xff09;爬取景点评论数据 1 爬取其他城市景点数据 只爬取一个城市的数据对于做数据可视化系统可能是不够的&#xff0c;因为数据样本量少嘛&#xff0c;本期来爬取其他城市的景…

1.Redis介绍

redis是一个键值型数据库。 是一种nosql数据库&#xff0c;非关系型数据库。 sql数据库 1.字段类型是固定的。 2.表的结构是固定的。表数据量特别大的时候&#xff0c;去修改表结构会出现问题。也会导致业务逻辑的修改。 3.每个字段有一定的约束&#xff0c;比如唯一约束&…