中断和异常

异常是我们经常听到的一个词,“系统发生了异常”,“是不是异常导致的?”。但是我们真的了解异常吗,或者有没有想了解一下他到底是什么,以及发生以后的一些细节呢?这篇文章就多个方面来展示一下“异常”。

中断我们也会常常听到,文章会先介绍中断异常的区别和共同点,以及跟他们关联的中断(异常)向量表,中断(异常)处理例程。

具体的,“异常”其实是cpu提供的一个机制——当cpu当执行指令过程中出错了,有可能是操作数不合理导致的,有可能是访问权限有问题等等,从而产生异常,接着操作系统会对这个异常进行分发处理。如果有调试器会中断到调试器。

操作系统的分发处理,下文会两个方面进行介绍: 

1)异常的核心的数据结构

2)异常分发机制

异常除了cpu异常外,还有软件异常。文章中也会涉及到寄存器的知识。

用一句话概括就是:在保护模式下,当cpu中断或者异常发生的时候,cpu(硬件)通过IDT来寻找处理函数,这便是操作系统(软件)的中断和异常机制开始的地方。


目录

中断和异常的简要介绍(cpu角度)

中断

异常

根据来源的角度来分类:

根据报告的方式和是否马上终止的角度来分类

异常——分类、异常登记,异常分发

描述异常的核心的数据结构

EXCEPTION_RECORD类

CONTEXT类

处理异常的核心的处理函数

内核函数和API:

引发异常过程:RaiseException 和 RtlRaiseException 和 NtRaiseException 和 KiRaiseException

分发异常过程:KiDispatchException 和 RtlDispatchException 和 KbgkForwardException 和 CommonDispatchException 和 KiUserExceptionDispatcher 

KeBugCheckEx(后面会用到,先看一下)

异常登记

cpu产生的异常

软件产生的异常

异常分发——软件异常和cpu异常一样

KiDispatchException对于内核态异常的分发过程

 KiDispatchException对于用户态异常的分发过程

一个简单的例子:

KiDebugRoutine

结构化异常处理(SEH)机制

向量化异常

中断和异常的共同点(cpu角度)

中断/异常向量列表

上面介绍了一下快速系统调用

中断/异常优先级

中断/异常处理

实模式下的中断向量表IVT

进入中断例程之前的中断过程:

中断例程:

保护模式下的中断描述符表IDT 

IDT中存的是段描述符,其实这里也叫门描述符,分成三种类型:

陷阱门和中断门的中断过程

64为模式下的IDT

断点和单步执行--《软件调试》

1.断点

硬件方法设置断点:

软件方法设置断点

单步执行

陷阱标志:

不同维度看单步

C++异常

1.关键字

2.标准库的异常的继承结构以及简要说明

3.函数声明后面的异常说明

4.为异常安全做努力

结构化异常 和 标准C++异常写法

_set_se_translator 函数——把SEH异常转换成C++异常

端口

in和out指令


中断和异常的简要介绍(cpu角度)

区别是来源不同:

中断来自于cpu外部的硬件触发,

异常来自于cpu内部的正在执行的指令本身。

(所以一些书中,会把中断叫做外中断;把异常叫做内中断。)

其实中断是很正常的,也不是错误,就是一个正常的机制,交互的机制。

异常在我们的概念中总是程序发生了错误需要找bug,其他也有一些正常的机制,比如缺页异常,断点异常。

中断:

外部设备发生需要cpu处理的信息的时候,相关的芯片cpu发出中断信息。cpu在执行完当前指令以后,检测到信息,引发中断过程

中断可以分成以下两种类型:

1)可屏蔽中断INTR:

大部分的外设的中断都是可屏蔽中断

通过IF是不是等于1来决定要不要执行完当前指令后便立即响应。=1,要立即。=0,不要

(注:在中断过程中需要将IF置为0。

原因就是让进入中断处理例程以后,屏蔽可屏蔽中断。

设置IF为1指令sti,设置为0的指令cti。)

2)不可屏蔽中断NMI:

必须立即响应。进入中断处理例程

总结:相当于外部设备cpu之间的通信过程,外部设备通过:

触发cpu的NMI引脚,引发不可屏蔽中断

触发cpu的INTR引脚,引发可屏蔽中断

这其中有个中断控制器,用来记录中断请求号的分配情况,相当于哪个外设来请求cpu的中断了:

IRQ0:分配给系统时钟

IRQ1:分配给键盘

IRQ4:分配给串口1和串口2

IRQ6:分配给软盘驱动器

IRQ9:多个设备共享

涉及到的中断过程,中断请求有个概念,后面会细说。

异常

在执行的指令中,检测到了与预先定义某个或者多个条件而产生的同步事件

下面分别从多个角度的分类来看一下异常。

根据来源的角度来分类:

1.程序写的本身的错误:(本质就是指令规范的检查)

                     1)操作数有错误:比如除法指令除数为0(在执行到除法指令的时候,内部就会检测是否除数是零

                     2)检测到指令规范中定义的非法情况:比如用户模式下执行特权指令

2.某些特殊的指令:(指令本身就是设计成产生异常的目的)

  •             int n:

比如 int 3 就是执行完了就会产生一个异常,叫断点异常,所以int 3也叫做断点指令

有的书中会把int n这种异常叫做软件中断

int n 其实就是取执行n号向量异常处理例程。n号向量见下文。

  •             into 
  •             bound

3.奔腾cpu引入的机器检查异常:当cpu执行指令期间检测到cpu内部或外部的硬件错误。书中的Chapter6(好像是软件调试这本书),大部分的错误异常。

根据报告的方式和是否马上终止的角度来分类

一般都用这个角度分类。

1.错误类异常:

           经典例子是缺页异常(cpu会把暂时不用的内存页从物理内存交换到外存中,或者一开始的时候还没有加载到物理内存中,当访问这些不存在物理内存中的页的时候就会产生缺页异常,异常处理程序就是内存管理器的函数把对应的内存页加载到物理内存里)。任务管理器中可以看到不停的在发生缺页异常。   

           

有些错误类异常也是不可以恢复的,比如栈错误异常esp超出了栈所在的段的边界了。因为会导致内部的状态,寄存器的值什么的都可能已经改变了。

2.陷阱类异常:

        经典例子是断点异常,int 3。该指令会使得cpu中断到调试器,从调试器返回后,被调试程序可以继续执行。

3.中止类异常:

        经典例子是机器检查异常

        比较严重的错误。硬件错误,或者是系统表中非法的值或者不一致的状态。

注:陷阱类异常,保存的cs和eip的值是【导致异常的那条指令的下一条指令】。但是不一定是那条指令的相邻的下一条指令,因为也可能是跳转指令或者是函数调用指令导致的异常。

(esp cs eip 寄存器 不懂的可以先看看相关的文章,也可以先忽略)

具体有哪些异常,见下面有个中断/异常向量列表


异常——分类(软件异常,硬件异常)、异常登记,异常分发

  • 硬件异常:上面说的异常时指的是cpu产生的异常,叫做硬件异常
  • 软件异常:还有一种通过软件方式模拟出来的异常(并不是真正cpu执行了错误的指令导致的)叫做软件异常 。比如调用 RaiseException API 和 throw 关键字抛出的异常 ,这类异常有C++产生的异常,.Net产生的异常等

下面对这两种异常的产生处理过程和分发过程的介绍。

描述异常的核心的数据结构

EXCEPTION_RECORD类

用来描述一个异常的结构:EXCEPTION_RECORD (winnt.h) - Win32 apps | Microsoft Docs

异常登记(后面看)的过程中会创建这样一个类的实例。

typedef struct _EXCEPTION_RECORD {
  DWORD                    ExceptionCode;   //异常代码,是一个32bit的整数,可以通过GetExceptionCode 宏获取的,具体参见https://docs.microsoft.com/en-us/windows/win32/debug/getexceptioncode,有哪些返回值,比如常见的0xC0000005L,非法访问。
  DWORD                    ExceptionFlags;   // 异常标志,每一位代表一个标志。比如第一位=1代表该异常不可恢复继续执行,如果试图恢复,会导致一个EXCEPTION_NONCONTINUABLE_EXCEPTION异常
  struct _EXCEPTION_RECORD  *ExceptionRecord;    //另外一个相关的异常,没有就是空
  PVOID                    ExceptionAddress; //异常地址。对于硬件异常:其值可能是下一条指令(陷阱类,比如数据断点触发的调试异常),也可能是本条指令(错误类,比如非法访问)
  DWORD                    NumberParameters;
  ULONG_PTR                ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD, *PEXCEPTION_RECORD; 

ExceptionCode 一些经典的异常代码

还有一些其他的值 比如说遇到过一个码是 0xe06d7363

STATUS_CPP_EH_EXCEPTION

C++ exception handling exception

0xE06D7363

Specific Exceptions - Windows drivers | Microsoft Docs

CONTEXT类

Contains processor-specific register data. The system uses CONTEXT structures to perform various internal operations. Refer to the header file WinNT.h for definitions of this structure for each processor architecture.

处理异常的核心的处理函数

内核函数和API:

内核函数就是内核函数,,,

API是提供给应用程序的接口函数。

引发异常过程:RaiseException 和 RtlRaiseException 和 NtRaiseException 和 KiRaiseException

  • RaiseException

来源:Kernel32.dll导出的API,供应用程序使用。

功能:从输入参数可以看出,这个函数的目的之一肯定是构建一个 EXCEPTION_RECORD 实例。除此之外,这个函数的做的事件就是调用 RtlRaiseException(下面这个)。

void WINAPI RaiseException(

__in DWORD dwExceptionCode, //可以是表11-2中的代码,也可以是自定义的整数

__in DWORD dwExceptionFlags,

__in DWORD nNumberOfArguments,

__in const ULONG_PTR *lpArguments );

  • RtlRaiseException:

来源:NTDLL.DLL 中的函数。

功能:当前执行的上下文(通用寄存器等)放入到CONTEXT,然后通过 NTDLL.DLL 中的系统服务调用机制,调用到内核中的函数 NtRaiseException(下面这个)。

  • NtRaiseException:

来源:内核中的函数。

功能:内部会调用到内核中的另外一个函数 KiRaiseException(下面这个)。

  • KiRaiseException:

来源:内核中的函数。

功能:通过 KeContextToKframes 例程,把ContextRecord 结构中的信息赋值到当前线程内核栈,把ExceptionRecord 中的异常代码ExceptionCode的最高位清0,以便区分软件异常cpu异常。最后调用 KiDispatchException 进行分发异常过程。

输入参数有:

1)指向ExceptionRecord 异常记录(EXCEPTION_RECORD)的指针

2)指向ContextRecord 线程上下文(CONTEXT 类)的指针

3)ExceptionFrame 对于x86总是0

4)TrapFrame:栈帧的基地址

5)bool FirstChance 表示是该异常的第一轮还是第二轮

总结:

上面说的是一个应用程序产生软件异常的过程:

用户态代码调用RaiseException——》

NTDLL.DLL 中的 RtlRaiseException——》

内核 中的 NtRaiseException ——》

内核 中的 KiRaiseException

如果是一个内核态的代码产生软件异常的过程:

内核态代码调用 RtlRaiseException(相当于NTDLL.DLL 中的版本 )——》

内核 中的 NtRaiseException ——》

内核 中的 KiRaiseException

具体对于C++程序抛出throw的异常(C++异常实现与编译器相关,讨论的时VC++编译器的情况):

CException派生而来的各个异常类,throw一个异常,对应的函数是== CxxThrowException ——》调用到 RaiseException ,其中 dwExceptionCode = 0xe06d7363 (对应的ascii码时.msc)(关于具体的C++异常后面会单独再详细介绍)

对于.Net程序抛出的异常(CLR异常):

也是调用到 RaiseException ,其中 dwExceptionCode = 0xe0434f4d (对应的ascii码时.COM

硬件异常:应该是异常处理程序中调用的异常处理程序,没有设计上面这些函数,直接就是Dispatch了。后面异常登记这一部分会介绍。

分发异常过程:KiDispatchException 和 RtlDispatchException 和 KbgkForwardException 和 CommonDispatchException 和 KiUserExceptionDispatcher 

  • KiDispatchException :

来源:内核中的函数。

功能:通过 KeContextFromKframes 例程,把TrapFrame 参数指向的 KTRAP_FRAME 结构产生一个 CONTEXT 结构。以供调试器异常处理函数报告异常使用。

在内核模式引发异常下(PreviousMode =0):

KiDispatchException 会调用KiDebugRoutine ,且在 KiDebugRoutine 返回False的时候,会调用RtlDispatchException

在用户模式引发异常下(PreviousMode =1):

KiDispatchException 会调用KbgkForwardException,且在 KbgkForwardException返回False的时候,KiDispatchException 会转回到用户态,执行异常分发函数(NTDLL.DLL中的KiUserExceptionDispatcher 

KiDispatchException 输入参数有:

1)指向ExceptionRecord 异常记录(EXCEPTION_RECORD)的指针

2)ExceptionFrame 对于x86总是0

3)TrapFrame:栈帧的基地址。用来描述异常发生时候的处理器的状态,各种通用寄存器调试寄存器段寄存器等。

4)PreviousMode =0,表示前一个模式是内核态,=1,用户态。即描述引发发生异常时候的执行模式是什么模式。根据这个值,走的是不同的路线。详情看下文的分发过程。

5)bool FirstChance 表示是该异常的第一轮还是第二轮。Windows系统最多分发两轮。

前面说到在用户模式下引发异常的情况, KbgkForwardException如果返回false,KiDispatchException 会转回到用户态,执行用户态的异常分发函数(NTDLL.DLL中的KiUserExceptionDispatcher )。这里注意3点:

首先:用户态的异常分发函数是记录在内核变量 KeUserExceptionDispatcher 中的,在windows系统中指向的是NTDLL.DLL中的KiUserExceptionDispatcher

其次:如何从内核态转会用户态执行:

1)KiDispatchException 确认用户态栈有足够的空间容纳CONTEXT结构和EXCEPTION_RECORD结构,然后复制到用户态栈一个线程的栈,分成用户态的栈和内核态的栈

2)确认好了以后,把TrapFrame 参数指向的 KTRAP_FRAME 结构调整为用户态执行的合适的值——段寄存器,栈指针,将KeUserExceptionDispatcher的值复制给KTRAP_FRAME 中的EIP字段(这样线程返回到用户态以后,就会开始执行KiUserExceptionDispatcher了。

3)KiDispatchException就返回了,软件异常和硬件异常的具体返回流程:

对于软件异常(我们说过软件异常的发起是通过调用RaiseException开始的,其异常处理和分发流程):

KiDispatchException返回到KiRaiseException,

再返回到NtRaiseException ,再通过系统服务返回流程返回用户,

因为KTRAP_FRAME 已经调整了,所以就会从KiUserExceptionDispatcher开始执行了,而不是本来调用系统服务的地方

对于cpu异常(也就是硬件异常,是通过异常处理例程进来的异常处理和分发流程):KiDispatchException返回到CommonDispatchException,

然后执行KiExceptionExit,并根据KTRAP_FRAME 恢复cpu的状态,而后执行异常返回指令(IRETD)。

因为KTRAP_FRAME 已经调整了,所以返回后就会从KiUserExceptionDispatcher开始执行了。

  • RtlDispatchException:

来源:如果是内核中的函数(ntoskrnl中的,内核函数)。

功能:取得异常注册链表。也就是异常处理器,遍历每个,处理了异常返回,没有处理继续查找。函数返回TRUE标识处理了异常FALSE表示没有处理该异常。

异常处理链表的首字节地址:FS寄存器指向的地址的前DWORD个字节

FS寄存器:存的是线程信息块TIB

对于程序中写的结构化处理异常__try __exception 就是一个异常处理器,后面会介绍到。

RtlDispatchException 会评估其中的过滤表达式,如果返回值是1的话 RtlDispatchException 会进行全局展开和局部展开(栈展开),然后执行异常处理块。

  • RtlDispatchException

来源:如果是用户态的函数(NTDLL.DLL中的,用户态函数)。

功能:函数返回TRUE标识处理了异常FALSE表示没有处理该异常。同样是取得异常注册链表,这个链表的尾部有一个系统注册的默认的异常处理器(KERNEL32.dll 中的 UnhandledExceptionFilter函数

UnhandledExceptionFilterkernel32.dll中用于处理未处理异常核心函数——他是系统在终止掉一个进程之前最后做处理的地方,应用程序错误JIT调试都是从这个函数发起的。

LONG UnhandledExceptionFilter(
  _EXCEPTION_POINTERS *ExceptionInfo
);

其中UnhandledExceptionFilter的返回值是long类型,其值具体有0,1,-1。(跟过滤表达式的返回值的含义一样,具体看下面)

如上图内核态的函数一样,如果异常处理器返回一直是EXCEPTION_CONTINUE_SEARCH的话,RtlDispatchException就是返回false

两个的基本功能和执行的逻辑是基本一样的。

  • DbgkForwardException(后面会用到,先看一下)

来源:用户态调试子系统内核例程

功能:输入参数:

1)ExceptionRecord 异常记录(EXCEPTION_RECORD

2)bool值,true=发给调试端口,false=发给异常端口。

3)bool值,是否是第二次机会。

如果发给调试端口,检查进程的DebugPort是否为空

不为空:调用DbgkpSendApiMessageDbgkpSendApiMessage会将异常发送给调试子系统调试子系统会将异常发给调试器

                   如果DbgkpSendApiMessage返回成功,且调试器处理该异常,那么DbgkForwardException返回true

                   否则DbgkForwardException返回false

为空:估计就是返回false

如果发给异常端口,检查进程的ExceptionPort是否为空。把异常发给异常端口(通常环境子系统会在创建进程的时候将该字段设置为子系统监听的一个LPC端口ApiPort,不懂),给那里的监听者一次处理异常的机会。

不为空:调用KbgkpSendApiMessageKbgkpSendApiMessage不会挂起该进程,但是会阻塞该线程。

                   如果KbgkpSendApiMessage返回成功,且端口处理了该异常,那么KbgkForwardException返回true

                   否则KbgkForwardException返回false

为空:估计就是返回false

  • KiUserExceptionDispatcher:

来源:用户态的函数了。

功能:会调用用户态的RtlDispatchException,(RtlDispatchException 上面介绍过了

如果用户态的RtlDispatchException返回:

返回true:KiUserExceptionDispatcher调用ZwContinue系统服务继续执行原来发生异常的代码,如果ZwContinue调用:

                  调用成功,便继续执行了啊

                  调用失败KiUserExceptionDispatcher会调用RtlRaiseException来抛出异常。

返回false:KiUserExceptionDispatcher调用ZwRaiseException并将FirstChance设置为falseZwRaiseException会调用内核服务NtRaiseException(内核态函数),然后NtRaiseException传递给KiDispatchException ,进行第二次机会的分发。

KeBugCheckEx(后面会用到,先看一下)

参数 BugCheckCode 引发蓝屏的错误代码。0xe1是未处理的内核异常。Windows对于未处理的内核异常,会调用KeBugCheckEx 函数引发蓝屏异常,报告错误,并终止系统。

void KeBugCheckEx(
  ULONG     BugCheckCode,
  ULONG_PTR BugCheckParameter1,
  ULONG_PTR BugCheckParameter2,
  ULONG_PTR BugCheckParameter3,
  ULONG_PTR BugCheckParameter4
);

异常登记

cpu产生的异常

首先通过IDT调用到异常处理例程KiTrapXX(是内核函数吧,是的

但是KiTrapXX只是做一些简单的表征和描述,

随后调用CommonDispatchException 函数,

并把异常代码,导致异常的指令的地址,其他参数通过寄存器传递给这个CommonDispatchException函数,

CommonDispatchException函数中,会分配一个EXCEPTION_RECORD结构,存传过来的信息。

准备好这个结构以后,在调用内核中的KiDispatchException 函数来分发异常。

cpu产生的异常,可能是用户态的代码,也可能是内核态的代码导致的,再调用KiDispatchException 中函数的时候会把执行模式(PreviousMode )作为参数传入的(上面介绍这个函数的时候讲过),而后执行该函数进行分发。

(ps:内核态代码触发的除零异常,系统有特殊的处理,不使用这里介绍的分发流程)

(ps:页错误异常要调用MmAccessFault函数,见下面的非法访问异常描述)

软件产生的异常

如何产生:

见上面的 RaiseException 和 RtlRaiseException NtRaiseException KiRaiseException

内核服务NtRaiseException 是产生软件异常的主要方法。

用户态通过RaiseException来调用此个内核服务。

异常分发——软件异常和cpu异常一样

为什么要分发:为了支持调试和软件自己定义的异常处理函数

分发给谁:调试器和应用程序自己的异常处理函数

发几次:同一个异常可以有两轮机会(KiRaiseException中的FirstChance输入参数),比如第一轮没有人处理的话(未处理的异常),就再来一次。

Windows对cpu异常和软件异常都是以相同的方式进行,但是会根据异常产生的代码是在用户态还是内核态,分成两种情况。

下面详细讲解分发过程:

KiDispatchException对于内核态异常的分发过程

第一次机会

1.KiDebugRoutine返回true,结束返回

2.KiDebugRoutine返回false,调用RtlDispatchException

        2.1 RtlDispatchException 返回true,结束返回

        2.2 RtlDispatchException 返回false,再次调用KiDebugRoutine(第二次机会)

                      2.2.1 KiDebugRoutine 返回true,结束返回

                      2.2.2 KiDebugRoutine 返回false,说明这是个未处理的异常,调用KeBugCheckEx,终止系统

第二次机会

                      KiDebugRoutine 返回true,结束返回

                      KiDebugRoutine 返回false,说明这是个未处理的异常,调用KeBugCheckEx,终止系统

 KiDispatchException对于用户态异常的分发过程

ps.其实最开始会判断是否需要发送给内核调试器(是否内核调试器触发,内核调试的设置中是否设置接受用户异常,如果是的话,通过内核调试会话,发送异常给i主机上的内核调试器),但就算发送,内核调试器一般也不处理用户态异常,所以返回不处理。

第一次机会

1.DbgkForwardException(debugport)返回true,结束返回

2.DbgkForwardException(debugport)返回false,转到用户态执行,用户态执行的过程中可能会发起第二次的异常

第二次机会

1.DbgkForwardException(debugport)返回true,结束返回

2.DbgkForwardException(debugport)返回false,调用DbgkForwardException(exceptionport),如果返回

             true:结束返回              

             false:终止当前进程(其实还有一次机会UnhandledExceptionHandler)。调用KeBugCheckEx,终止系统。

一个简单的例子:

当用户态代码发生异常时,比如内存访问违规时,CPU执行指令的时候会发现此问题,并产生一个异常,然后CPU会把代码流程切换到异常处理服务例程,异常处理服务例程会进行异常分发:

查看当前进程是否处于调试状态(比如是否有JIT)

           如果是,则通知调试器发生了异常

           如果不是,则操作系统会查看当前线程是否安装了异常帧链(FS[0],栈帧),如果安装了SEH,则调用SEH,并根据返回结果决定是否全局展开局部展开。如果异常链中所有的SEH都没有处理此异常,而且此进程还处于调试状态,则操作系统会再次通知调试器发生异常(二次异常)。

如果还没人处理,则调用操作系统的默认异常处理代码UnhandledExceptionHandler,不过操作系统允许你Hook这个函数,就是通过SetUnhandledExceptionFilter函数来设置。

大部分异常通过此种方法都能捕获,不过栈溢出、覆盖的有可能捕获不到。

SetUnhandledExceptionFilter 函数——设置一个回调函数,用来设置对于未经处理的异常的处理函数

SetUnhandledExceptionFilter function (errhandlingapi.h) - Win32 apps | Microsoft Docs

LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter( LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter );
  • 输入参数:LPTOP_LEVEL_EXCEPTION_FILTER 是设置的回调函数的签名类型,具体如下:

typedef LONG (WINAPI *PTOP_LEVEL_EXCEPTION_FILTER)(
    __in struct _EXCEPTION_POINTERS *ExceptionInfo
    );
typedef PTOP_LEVEL_EXCEPTION_FILTER LPTOP_LEVEL_EXCEPTION_FILTER;

跟 UnhandledExceptionFilter 的签名是一样的。

  • 返回参数:也是LPTOP_LEVEL_EXCEPTION_FILTER ,那么代表的是上一次通过SetUnhandledExceptionFilter设置的回调函数的地址:The SetUnhandledExceptionFilter function returns the address of the previous exception filter established with the function. A NULL return value means that there is no current top-level exception handler.

KiDebugRoutine

是什么?内核变量,标识内核调试引擎交互接口函数。根据是否启用内核调试引擎,分别指向不同的函数:

启用:指向KdpTrap函数,如果内核调试器处理了该异常,那么函数返回True,否则False。

没有启用:指向KdpStup函数,KdpStup函数做一些简单的操作,然后返回False

根据KiDebugRoutine的返回值不同分别做不同的事情

返回false:说明灭有处理该异常,会去调用RtlDispatchException(上面有伪代码),去找已经注册了的结构化异常处理器,去分发。

返回true:停止分发,准备返回。

什么叫未处理的异常

什么叫结构化异常处理器

结构化异常处理(SEH)机制 Structured Exception Handling

结构化异常处理SEH是Windows操作系统所提供的对错误或异常的处理机制,是Windows操作系统的一个重要组成部分。

SEH广义的含义:异常分发的机制,如上面的流程。函数实现和关键数据结构。相当于提供了一个框架。比如:

位于内核态的KiDispatchException 和 NtRaiseException

位于NTDLL.DLL的KiUserExceptionDispatcher

异常注册链表的表头是登记在每个线程的TIB中的

SEH狭义的含义:可以理解为操作系统的异常机制的对外接口,有了这个接口,程序员在代码写异常处理器。相当于从使用者的角度,提供了一个框架。

  • 终结处理:以VisualC++编译器为例子,不同编译器支持的SEH机制的关键字可能不同

__try

{

     // 被保护体,也就是要保护的代码块

     // 被保护体的退出分成 正常结束(自然执行顺序进入后面代码)和非正常结束(发生异常,return,goto,break,continue等离开被保护块代码),注意不管是什么退出,即使是return,goto,终结处理块都会被执行。

     // 还可以使用__leave关键字的退出,属于正常退出。__leave关键字只能用在被保护体中。

}

__finally

{

    // 终结处理块只要不是被保护体中的代码 终止了当前线程ExitThread ExitProcess 退出线程或整个进程),终结处理块都会被执行。

    // 可以使用AbnormalTermination来判断是不是非正常结束。这个函数只能用在终结处理块中。

}

  • 异常处理:

__try

{

     // 被保护体,也就是要保护的代码块

}

__exception(过滤表达式)   

// 过滤表达式,是个表达式,不管你干了啥,只要表达式的结果是0 1 -1这三个值。

{

    // 异常处理块只要不是被保护体中的代码 终止了当前线程ExitThread ExitProcess 退出线程或整个进程),终结处理块都会被执行。

    // 可以使用AbnormalTermination来判断是不是非正常结束。这个函数只能用在终结处理块中。

}

ps1.过滤表达式的结果是0 1 -1的含义

1).EXCEPTION_CONTINUE_EXECUTION (–1) :已经处理了异常了,控制流将在异常出现的点之后,继续恢复运行。

VC++编译器把所有的C++异常都强制规定为不可恢复的执行的,即对于这样的异常,过滤表达不准返回-1。后文会介绍C++异常,就是C++异常转化为seh异常以后不会是这个值

如果强行返回,会导致EXCEPTION_NONCONTINUABLE_EXCEPTION异常。

2).EXCEPTION_CONTINUE_SEARCH (0) :异常不被识别,也即当前的这个__except模块不是这个异常错误所对应的正确的异常处理模块。系统将继续到上一层的try-except域中继续查找一个恰当的__except模块

3).EXCEPTION_EXECUTE_HANDLER (1) :异常已经被识别,也即当前的这个异常错误,系统已经找到了并能够确认,这个__except模块就是正确的异常处理模块。控制流将进入到__except模块中。 

ps2.有些宏只能在过滤表达式异常处理块,比如GetExceptionCode 

ps3.有些只能在过滤表达式中用的宏,比如GetExceptionInformation

#define GetExceptionInformation     (struct _EXCEPTION_POINTERS *)_exception_info

typedef struct _EXCEPTION_POINTERS {
  PEXCEPTION_RECORD ExceptionRecord;
  PCONTEXT          ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

一般使用:

__try 
{ 
     // try block 
} 
__except ( FilterFunction(GetExceptionInformation() ) //FilterFunction函数自己定义的
{ 
    // exception handler block 
}


 //FilterFunction函数自己定义的
bool FilterFunction(PEXCEPTION_POINTERS pException)
{
    // 处理函数 通过对 pException的两个成员 ExceptionRecord 和  ContextRecord 记录一些可读信息 略
}

  • 嵌套的使用终结处理和异常处理:好像不太看到,不写了。

向量化异常

向量化异常(VEH)和结构化异常(SEH)的区别和联系


中断和异常的共同点(cpu角度)

共同点:cpu不再接着往下执行,而是转去执行对应的处理程序(中断处理例程,其实准确的应该说是中断/异常处理例程也有的时候叫做异常处理例程)

由统一的方式进行响应和管理。在系统中定义一个向量数来标识每个中断或者异常。

(ps:上面说的软件异常就不包括在以下内容了。)

中断/异常向量列表

ps1

向量号为1的异常来源

比如debug中执行t命令就是引发单步执行中断,其作用是设置标志寄存器的TF=1

当cpu检测到tf=1以后,当cpu执行完一条指令以后引发单步中断——单步中断的中断处理程序是打印当前的所有寄存器在屏幕上。

ps2

数据断点触发的调试异常,属于陷阱类异常

ps3

写程序的时候最常见的非法访问异常来源:

1.主要来源于CPU的页错误异常#PF(14) (页错误异常内核处理例程KiTrap0E)

2.也有可能来源于由于系统检测到的其他违反系统规则的情况。

比如一个变量没有指向有效的内容,就对他进行写。那么就会进行一系列操作以后(包括有把错误代码压入栈的操作),就调用KiTrap0E,KiTrap0E从压入栈中的错误代码取出发生模式,访问内存的方式(读,写或者取),然后调用MmAccessFault函数检查异常发生的原因,如果发现是指向不允许访问的内存空间的时候,就会构造生成C0000005的异常,送到KiDispatchException进行分发。

ps4

int n 指令(软件中断)其实就是取执行n号向量的处理例程n就是n号向量。其中ah寄存器还可以传递一个二级的参数,n号中断类型码这个中断处理过程中的ah号子程序

比如 int 21H 是程序返回指令:21H号中断例程,4C号(ah的值)子程序。功能为程序返回,返回值是00(al)。

    mov ax 4c00h

    int 21H

比如 int 2e 对应的中断例程是 分发系统调用的例程 >!idt 2e 。 

子程序号即这里是系统服务号0xa1(虽然每个系统服务都是唯一的编号,但是不保证在所有版本都是一致的)

用户模式切换到内核模式——int 2e

应用程序调用API——ReadFile,调用到Kernel32.dll中的ReadFile,再调用到Ntdll.dll中的ReadFile,发起软件中断int 2e,调用到内核态的中断处理例程KiSystemService(通过IDT表得到其函数地址)

然后KiSystemService通过系统服务号,从系统服务分发表,找到要调用的服务函数地址和参数描述。

然后将参数从用户态栈赋值到该线程的内核栈中。

然后调用到真正的NtReadFile函数。

操作结束,返回到KiSystemService,KiSystemService又负责把操作结果复制回到用户态栈中,然后通过IRET(中断例程返回的指令)返回到NtReadFIle中,

然后继续执行上图int 2e 后面的指令。

因为中断例程内核态的函数,所以,cpu在执行到内核态空间之前,会进行模式切换的准备,用户模式切换到内核模式:

1)权限检查,检查源位置和目标位置所在的代码段权限,核实是否可以转移。

2)准备内核模式使用的栈,为了保证内核空间的安全,所有线程在内核态执行的时候必须使用位于内核空间的内核栈,一般大小8k,12k。

上面说的int 2e是用户模式到内核模式的方式, 

另外一种从用户模式切换到内核模式——快速系统调用

因为通过int 指令,每次都需要查找IDT找到中断例程的地址,也就是说要读取内存。所以有没有办法通过寄存器来存这一个值?有,快速系统调用就是,当cpu支持的时候,就使用这种方式切换模式。使用cpuid指令检测是否支持。如果支持的话,会有如下准备工作做好:

1)全局描述符表中有4个段描述符。

描述sysenter指令进入内核模式的代码段。 描述sysenter指令进入内模式的栈段。 描述systemexit指令退回用户模式的栈段。描述systemexit指令退回用户模式的栈段。按顺序排列,找到一个就能推算出其他的。

2)三个MSR寄存器,分别指示sysenter指令要跳转到的目标例程(KiFastCallEntry)的地址,其代码段的选择子,和新的栈指针EIP(具体见汇编指令的MSR寄存器文章)

3)固定内存段的SharedUserData模块(映射到win32进程中的用户空间),有一段SystemCallStub的代码。

具体快速调用的流程:前面都一样,不一样开始:

1)ntdll.dll会调用到SharedUserData!SystemCallStubSystemCallStub调用sysenter指令

2)sysenter指令调用到KiFastCallEntryKiFastCallEntry又会调用到KiSystemService,后面都一样了。

还可以观察下SystemCallStub的mov edx, esp把内核模式返回以后的栈指针给保存起来。到时候返回以后进行恢复。

除法溢出中断 就相当于执行了 int 0

单步执行中断 就相当于执行了 int 1

上面介绍了一下快速系统调用

ps5

IA-32规定:

0-31号向量cpu设计者使用

32-255号向量操作系统和其他软硬件开发商使用。

中断/异常优先级


中断/异常处理

这里说的也是cpu相关的中断和异常。跟软件异常无关。

(前面我们说过异常分成,硬件异常和软件异常)

这里分成不同模式下的区别。

实模式下的中断向量表IVT

具体哪个中断/异常去哪个位置执行对应的处理程序,在内存中有中断向量表,表中存着对应的处理程序的地址,即中断例程的地址。

中断向量表的在内存中的位置是0:0,中断向量表的每一项都是4字节高2字节中断处理程序段地址低2字节中断处理程序偏移地址。一共256个项。所以IVT大小就是1KB,固定在物理地址0~1024。

如果要修改中断处理程序,就把其内存地址写到这个向量表的表项中。

中断处理程序也叫中断例程中断例程BIOS 和  DOS操作系统)在启动过程进行安装到到内存中(比如:21H号中断例程 由 DOS提供,10H号由BIOS提供),即将其地址安装中断向量表中。

中断例程里面还可以写中断指令,比如和硬件设备相关的DOS中断例程中一般都调用了BIOS的中断例程。

进入中断例程之前的中断过程:

1)进行一些检查。

2)pushf(压入标志寄存器)

3)设置标志寄存器的IF=0 (IF是可屏蔽外中断相关的

4)设置标志寄存器的TF=0,RF=0,AC=0。(TF=0这里是为了避免再次出现单步执行中断做的处理,如果不设置了那么在中断处理程序的每一条指令执行以后,岂不是都要产生单步中断了,所以要设置。)

5)push CS,push IP

6)取得向量号n,在IVT中找到对应的表项。

设置(IP)=(N*4),(CS)=(N*4+2),开始执行对应的代码,处理程序。

例子:int3的执行过程。#GP保护性异常。#SS栈错误异常

中断例程:

1)保存用到的寄存器(push)

2)处理中断

3)恢复用到的寄存器(pop)

4)用iret指令返回 (依次执行popf,pop IP,pop CS,相当于中断过程中的反向操作)——中断过程其实可以理解成函数调用,相当于另外一个地址执行代码,执行完中断处理程序以后iret返回到原来的地方。

保护模式下的中断描述符表IDT 

上面的IVT的位置是固定的,而IDT的位置是不固定的。

IDTR寄存器相关的见 段机制(段描述符)和页机制(内存分页)_u012138730的专栏-CSDN博客

>!idt -a ——windbg可以看IDT的各个项

IDT是一个cpu处理器一个的。在多核cpu中cpu分成bsp(bootstrap processor)ap(application processor),先是bsp完成了内核初始化和其他一些初始化以后,再初始化其他ap。所以显示bsp建立一个IDT以后,再复制给其他ap,并作必要的修改。

IDT会记录在pcrprcb

PCR:处理器的控制区(内核数据结构 KPCR)——windbg命令 >!pcr   >dt nt!_KPCR pcr的起始地址

PRCB:处理器的控制块

IDT中存的是段描述符,其实这里也叫门描述符,分成三种类型

(每个描述符都是8个字节,一共一般都是256个项,所以IDT大小一共是2K)

1.任务门描述符:表示的就是一个任务门的描述符了,就是从一个任务切换到另外一个任务。任务门就是一个TSS段,所以任务门描述符中有TSS段的段选择子TSS段是任务信息的一段内存区域。TSS段格式是cpu定义的,下面是IA-32cpu的定义:

               

             (寄存器,通用寄存器,控制寄存器对应的值,SS0-SS2和ESP0-ESP2对应的是不同的优先级执行时对应的栈。)

              关于TSS段,Windows操作系统的TSS段的个数其实并不是等于线程的个数。跟cpu相关的。每个cpu会分配4个tss段,一个处理nmi,一个处理#df异常,一个处理机器检查异常,一个供线程共享。切换线程时,将当前线程切换到共享的TSS中。普通的线程切换不会切换TSS。(每个线程的TSS段相同的?不会的吧,每个线程的基本信息应该不一样吧,或者是虽然TSS不变,估计就是不是通过切换TSS段来切换线程吧。所以任务是线程还是进程。。)                  

             切换任务过程:通过硬件方式切换。

                          1)通过任务门描述符的段选择子,找到TSS段的描述符,检查这个段描述符的存在标志为1,边界大于0x67,B表示不为1等

                          2)第一步检查通过后,cpu将当前任务的任务状态保存到当前任务的TSS段(当前任务的TSS段怎么找呢,他的段选择子是在TR寄存器中记得的)

                          3)把新任务的段选择子(就是任务门描述符中的段选择子)加载到TR寄存器中,然后把新任务的寄存器信息加载到物理内存中。然后开始执行新任务。

             双误异常,NMI,机器检查异常 对应的 IDT中的门描述符就是这种类型的。也就是向量2,8,18号是用任务门机制来处理的,除了这三个外,其他都是用中断门或者陷阱门来处理的。

2.中断门描述符

             对于中断门,cpu在将标志寄存器的当前值存入栈保存以后,在开始执行处理函数之前会自动清除标志寄存器的IF位。也就是屏蔽中断。

             中断门和陷阱门都是在当前任务的上下文进行切换,可能会有堆栈的切换。

3.陷阱门描述符

             对于陷阱门,不会屏蔽中断。

看下图具体的三种类型的描述符的内容。其中拉姆高四个字节的8-12位。表示的是一个描述符的种类,任务门,还是中断门,还是陷阱门,其中D=0表示的是16位门,D=1表示的是32位门(看到有段内偏移地址 高16位 低16位)

P:段存在标志

DPL:描述符优先级,优先级控制

段选择子+偏移地址:对于中断门和陷阱门来说,是中断处理例程的地址

                                  对于任务门来说(tss段选择子就行了估计,没看到有偏移地址),是tss段的地址。

陷阱门和中断门的中断过程

对于之前的中断/异常向量列表每个向量都有一个中断/异常处理例程。每个例程的段信息都是存在于IDT表中。

1)CPU根据向量号和IDTR找到对应的门描述符,然后通过一系列的检查,

2)检查后,如果目标代码段的特权级别比当前级别高,那么要切换栈,否则不需要。切换栈的操作:cpu从当前任务的tss段中取出新的堆栈的ss和esp,加载到ss和esp。旧的ss和esp就压入到新堆栈中。然后:

                1)将eflags cs eip压入栈。把cs 和 eip设置为要执行的内容

                2)如果发生的是异常,而且这个异常具有错误代码,那么把这个错误代码也压入新的堆栈中。

64为模式下的IDT

每个表项16个字节,原来是8个字节,即新的门描述符是16个字节了。在增加的高8个字节中,高4个字节保留,低4个字节记录处理函数地址的高32位。

IDT中没有任务门,只有中断门和陷阱门。

所以当发生双误异常的时候,不是通过硬件切换线程了,而是通过一个IST机制来切换栈。IST机制就是在门描述符中有一个索引IST的位域(第二个word的bit0-2),值0-7。0表示不用切换栈,1-7表示IST表中的索引号。IST表是一张指向栈的数据指针,每个表项大小是64位,8字节。

>!idt 中看到有stack=xxx,代表就是发生这种异常/中断的时候要进行切换栈。


断点和单步执行--《软件调试》

1.断点

引发中断的点,用到引发异常的原理,配合调试器

断点的分类
1)设置的空间
    内存空间:代码段数据段
    IO空间:IO断点

2)设置断点的方法: 
    通过调试寄存器设置的硬件断点

    通过int 3设置的软件断点
    

    都是使得cpu执行到指定的位置或者访问指定的位置的时候中断到调试器


硬件方法设置断点:

386首次引入 8个调试寄存器 DR0-DR7 。 32位下32位,64位下64位。本身时64位的。不是依靠软件指令,而是处理器本身的 所以称为硬件断点


DR7 —————1个调试状态寄存器。定义断点的中断条件。有4组状态位置,分别对应DR0-3 比如要监控的长度,是访问IO地址 还是数据 还是代码段的时候出错。
                指令int 1 ————产生 #DB 调试异常。硬件断点产生的是 int 1 (下面说的 int 3 是软件断点产生的异常。 所以IA-32 分配了两个中断向量来支持软件调试,int 1和int 3)
                就算是产生了 int 1 。也分成各种不同的情况。有错误类异常陷阱类异常(异常的分类)等等后面介绍。 


DR6 —————— 1个调试控制寄存器。向调试器报告事件的详细信息。
                上面说了 同样是int 1 但是也是分成好多不同的情况(单步调试,调试异常的各个情况,)。所以怎么区分呢,就是用这个调试控制寄存器DR6中的各个位来判断到底是什么原因触发的异常
                比如 数据访问断点是陷阱类断点。

                            
DR4和DR5
DR0~DR3 ————4个调试地址寄存器,内存地址 IO地址
    

例子

代码中设置硬件断点:

注意:

只有当执行到程序入口了,才能设置硬件断点。可以再 main 和 WinMain 以后,再设置。或者在 系统的线程启动函数 比如 ntdll!RtlUserThreadStart  。(肯定是硬件断点记录的信息跟线程有关系)
        

vs2005这种用户态调试器 : 通过访问线程上下文( CONTEXT ) 数据来间接访问调试器

CONTEXT 是 用来保存线程的执行状态的,有通用寄存器的值,调试寄存器的值等等  。当线程挂起的时候 CONTEXT会被保存, 恢复再恢复。
可以通过 CONTEXT 设置调试寄存器的值 

如何使用调试寄存器来设置 数据访问断点 等硬件断点
使用 调试器 windbg 的 ba 命令  拍照

            

软件方法设置断点


int 3 -----是 x86 cpu提供的用来支持调试的指令。

软件调试中经常用到的“断点”功能,就是插入int 3 这条指令,所以这条指令也叫断点指令。

这条使得cpu中断到调试器。这条指令的机器码是0xCC(一个字节,区别与int n(n=3)的机器码 2个字节)


那怎么加int3呢?
            1)手工注入代码,写入了源代码中
                _asm INT 3 
                int 3(0xCC)和 普通的int n( n=3, 0xCD03 ) 的机器码是不一样的。编译器看到int 3 会编译成0xCC 而不是0xCD03
            
            2)在调试器中加入断点,一般IDE中就是鼠标点一下,单独的调试器比如windbg是bp指令
        
        在调试器中加入断点 断点的设置信息(断点所在的行位置和所在的文件)先保存在一个opt文件中。只有当启动调试的时候,把真正把opt中的断点信息设置到目标代码的内存映像中(这一步叫做落实断点),并将原本该位置对应的指令的第一个字节先保存起来。
                如果对应不找不到,比如设置了注释行,这个就是无效断点了。
 

当cpu执行到 int 3指令时
            虽然 int 3 已经是一条汇编指令 但底层对这条汇编指令的执行还有个过程,
            这条指令的最终的目的是去执行 #BP 产生断点异常,转去执行中断异常处理例程
            这里就设计到了 中断处理的过程了 转去执行异常处理例程之前可能会发生  #GP 保护性异常 #SS 堆栈异常    等异常
    
        int 3  断点异常 是处于陷阱类异常—— 陷阱类异常特点:当异常发生时,指向导致异常的下一条指令。但是int 3 不同,他指向当前指令:
            Windows 保护模式下 int 3异常的处理函数是内核的 KiTrap03 调用到的过程中还涉及用户态到内核态的转换
            操作系统对 int 3 这个中断处理例程做了 KiTrap03 特殊修改,使得程序指针寄存器减去1(因为 int 3的机器码时0xCC,就是一个字节),把原来的指向下一条指令,变灰指向本条指令
            之前说的对 int3 特殊处理, 使得 程序指针寄存器 指向当前指令, 就是在 KiTrap03 函数中 进行设置, 也就是让指针寄存器的值减1即可。

  
        int 3 虽然是一个特殊的软件中断,但涉及到异常分发的异常返回的知识跟普通的异常大体流程还是相同的。
        
        只是调试器在用户结束观察想要恢复执行的时候,需要做一些处理,让其落实所有断点(即把所有的断点列表处替换回int 3。因为中断到调试器以后是替换回原来指令了,因为要观察嘛),又能让继续执行。
        这里用了一个“单步走出断点”的技巧。
        
        
        
        
软件断点的局限性:( 依靠插入指令和软件中断机制 )
        1) 属于代码类的断点,让cpu执行到某一个位置的时候停下来。不适用于数据段和io空间
        2) 不能用于只读存储器
        3) 在中断向量表或 IDT 没有准备好的情况下不能使用
        
从操作系统角度看
            异常分发 KiTrap03
            提供了断点api: 以下api封装了 int 3 
                用户模式下 DebugBreak
                内核模式下 DbgBreakPoint 和 DbgBreakPointWithStatus
                
从编译器角度看
            是否觉得0xCC的机器码很眼熟呢
            为了辅助调试,编译器在编译调试版本的时候会用0xCC来填充刚刚分配的缓冲区,还有未初始化的,代码末尾段的空闲区域。这样如果 程序指针 指向了这些地方,就会马上中断到调试器了。


从调试器角度看
            >bp 一个地址
            当通过bp设置断点注意:
            1) 不能把断点设置在某条指令的中间
            2) 不能把断点设置在变量上
            
windbg相关指令集:
        g ————————继续执行
        r eip 值——————修改寄存器 。 没有 值 就是直接看寄存器的值多少
        ub 地址 l2 ———— 看前两条指令是什么 比如地址 是 一个栈帧的返回地址 就可以看前一个函数 调用这个函数前执行的代码
        uf/u 函数名 ———— 模块名!一个函数名
        u 地址————反汇编这个地址 也可以是u 
        bl ————— 看断点的设置的命令

调试器的机制    
    中断到调试器以后的上下文:CONTEXT

    追踪点:中断以后,然后设置一些打印的信息,自动恢复,让继续运行

    条件断点:中断以后,检查是否满足,不满足继续运行,满足停止运行
    
    

单步执行

陷阱标志:

标志寄存器中陷阱标志位TF
任务状态段TSS段的T标志

        
不同于设置断点的方法,另外一种中断到调试器的方法————设置 陷阱标志
比如单步执行标志,EFLAGS 的 TF 位 ( 按 F10 执行, IDE会帮我们设置 TF 位 )

设置TF位,没每执行完一条指令,就会产生一个#DB 调试异常 就是 int 1 由于设置TF为=1 产生的 调试异常 也叫做 单步异常。 前面 记得说硬件断点的时候产生的也是 int 1

错误类异常:硬件断点中的指令访问异常
陷阱类异常:软件断点异常#BP 硬件断电中的数据以及I/O断点异常 还有这里说的单步异常。

不同维度看单步


    汇编级别      ——设置TF=1
    源代码级别   ——IDE一般用的 。具体实现有好几种办法,也是根据汇编级别的设置TF 或者 动态插入 int 3,区别就是要不要中断给用户,或者判断高级语言的最后一条汇编语句或者是下一条高级语言的起始指令
    程序分支级别——windbg的tb命令 ( 需要硬件的支持 , 以及 在调试wow64程序的时候 不可用 ) 。 学名: 按分支单步执行。(之前的单步执行叫单位单步执行)
        如何进行按分支单步执行 —————— 同时置起 TF 位 和 BTF 位 ———— 当检测到有跳转(分支,或异常),中断到调试器
        BTF 位: MSR 寄存器的 位1 中。
        MSR寄存器 在P6处理器 叫DebugCtrlMSR
                              在奔腾4中 叫DebugCtrlA
                                奔腾M中 叫DebugCtrlB
        如何设置 BTF 位 : 只有内核代码才能访问 MSR (通过RDMSR WRMSR)寄存器, 用户模式下 使用 WirteMSR 函数 API
 


    每执行一个任务或线程——设置任务状态段TSS段的T标志。一般调试都没有无提供。

                任务状态段TSS段:
                    记录一个任务。任务是cpu可以独立调度和执行的程序单位。
                    任务中记录 通用寄存器的值 段寄存器 和 其他重要信息
                    字节偏移为100的16位字(word)的最低位是T标志位,当设置为1时,cpu切换到这个任务就会产生调试异常
                    怎么判断是任务切换异常:通过DR6的BT标志。看上面的图。


    

注意:

之前说 int 3 可以通过手动设置代码 设置进去
那 int 1可以吗——不可以,int 3是特殊对待的,允许在用户模式下执行。int 1 就是 int n 指令,机器码是 0xCD01 ,在保护模式下,如果hi行 int n指令当前的 CPL ,大于 引用的门描述符的 DPL,就会产生 通用保护异常 #GP 
int 1 对应的中断门描述符 的 DPL 是 0, 只有内核模式下的应用程序可以写这个

关于TF 还有一点 要注意: 就是 int n 和 into 指令会清楚 TF 标志 。关于 TF 好像别的文章 写过 可以看看的 复习以下 还有什么中断门 之类的


C++异常

在写C++程序的时候,我们除了要写正常的逻辑外,还需要写对于错误情况和错误检测的代码。try catch 就是用来实现错误检测的所需要写的代码。

try catch 本质就是通过抛出一个软件异常,然后经过系统的异常分发机制,查找异常处理器,从而进行进一步的处理。

C++异常肯定需要C++编译器的支持,看看C++编译器是异常处理的。

1.关键字 throw try catch

前面说过throw其实就是调用CxxThrowException函数。

那么throw后面可以跟什么呢?

跟的是一个对象,也可以是一个表达式,只要表达式的结果是一个对象就可以了。

最常见的跟的对象就是C++标准异常类以及派生类了(后面具体介绍)

那么在throw的时候,会发生什么事情呢?

上面说throw后面跟的是一个对象,在throw表达式执行后,该对象会初始化创建一个异常对象(该对象需要是可复制的),这个异常对象是由编译器管理的。

如果throw表达式后面是一个数组或者函数类型实参的会怎么样?

特别注意,他们相当于是一个指针,如果该指针指向的内容是局部存储的话,那么在异常处理的时候肯定已经不复存在了(也就是说抛出指向局部对象的指针就是错误,运行时肯定会报错,虽然编译的时候不会给你报错)。

因为在抛出异常的时候会释放局部变量,而且在查找异常处理器(怎么查找,就是栈回溯)的时候会进行栈展开。在抛出异常以后就会对局部对象进行调用析构函数(如果有必要,必要的意思就是是个类,不是内置类型的。),在栈展开的过程也是会对局部对象进行正常流程销毁(正常的在每个函数退出前都是会对它的局部存储类类型的进行调用析构函数。猜测应该本质上,应该使用了嵌套的,__try __finally __exception)。

因为他只会对进行析构函数,如果是一个指针=new 类,那么他不会自动调用delete,所以也就是如果抛出异常了,跳过了执行delete的逻辑,而跳出到外面执行了,这个内存就成了内存泄漏,没有人管理的了。要特别小心,后面会讲过一个RAII的技术,来避免这种情况。

这里还说到会调用析构函数,那么析构函数本身如果抛出异常了呢,就是在这个异常还没有处理的情况下,又抛出了一个新的异常,这不是乱套了么,所以出现这种情况就会中止执行了,所以析构函数不应该抛出异常标准库类型都保证他们的析构函数不会抛出异常,为我们做了榜样。

如果真的没法保证析构函数中不会抛出异常,那么在析构函数中处理这个异常——直接终止程序或者消化异常继续运行——不要析构函数中的异常逃出析构函数

最好是把会抛出异常的代码写到别的普通的函数中去。

throw 后面的对象类型的话如果是有继承关系的类:

那么被抛出对象的静态编译时类型,决定了异常对象的类型(抛出一个对指针的解引用,但是这个类又是个多态的用法,那么会出现分割现象,就是只抛出了基类部分)

catch后面写什么呢?

catch后面的可以只是类型名,也可以接受传进行的实参,也就是说(类型名 形参)。一般都会用引用,因为如果不用引用的话,又会是赋值,如果在catch中重新抛出这个异常,throw;那么在catch中对这个异常所做的修改都不会生效

我们说过异常对象是由编译器管理的,所以我如果想修改这个对象,就必须对它进行引用操作(相当于指针操作)。

还有如果不是引用的话,对于继承类使用,throw 派生类,catch 基类 就会出现分割现象。

匹配哪一个catch块呢?

catch呢就是按一个一个按顺序进行匹配的,找到了合适的就不在查找了。那么怎么样算找到可以了呢,这里不需要抛出的对象类型catch中的异常说明符一模一样,可以由:

1.非const const 的转化

2.派生类到基类的转化

3.数组转化为数组类型的指针,函数转化为指向函数类型的指针。

(相当于过滤表达式中,返回1,表示由我来处理)

catch(...)表示什么任何类型的异常我到符合,肯定是写在catch的最后一个了。

try 
{  
    包含可能抛出异常的语句;  即throw 一个类
}  
catch(类型名& 实例名)    // 可以是特定的异常实例  
{  
  
}  
catch(类型名)    // 也可以只是特定类型
{  
  
}  
catch(...)    // 三个点则表示捕获所有类型的异常  
{ 
}  

Microsoft 的Visual C++编译器使用操作系统的结构化异常机制来实现C++异常处理机制。

编译器会为我们:

         一个C++ try 块时===》生成一个SEH   __try块。

          C++的catch语句===》对应SEH异常过滤程序,

         catch 块中的代码===》对应SEH __except 块中的代码。

             C++ throw语句===》生成对Windows RaiseException函数的调用。

throw语句所使用的变量===》成为RaiseException的附加参数。

如下例子:

//该函数使用C++异常处理

void ChunkyFunky()
{
    try
    {
         // try body
        ....
        throw  5;
    }
    catch( int  x)
    {
      // catch body
      .....
    }
    ....

}



//该函数则演示了C++编译器生成的对应的结构化异常处理代码

void ChunkyFunky()
{
  __try
  {
    // try body
     ......
    RaiseException(Code = 0xE06D7363, Flag = EXCEPTION_NONCONTINUABLE, Args = 5);
  }
 __except( (ArgType == Integer) ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH )
 {
   // catch body
   .....
 }
  .....
}

说明:

传给RaiseException 的异常代码为0xE06d7363,这是Visual C++开发组选择用来抛出一个C++异常的软件异常代码。上面说过。

Microsoft   Visual Studio的调试器为异常调试提供了出色的支持。当进程中的某个线程抛出异常,操作系统会马上通知调试器(如果调试器已经附着)。

这个通知呗称为“首次机会通知”。调试器将响应这个通知,促使线程寻找异常过滤程序

如果所有的异常过滤程序都返回EXCEPTION_CONTINUE_SEARCH,操作系统会给调试器一个“最后机会通知”。

​​​​​​​这两个通知的存在使得软件开发人员能更好地控制异常调试的过程。

2.标准库的异常的继承结构以及简要说明

语言层面就阻止了进一步调用到cpu指令层面的错误,提前告知,你这么写是会有错误的。

1.下面是标准库中定义好的异常类

2.也可以自定义一个类,继承自std::exception

#include <iostream>
#include <exception>
using namespace std;
 
struct MyException : public exception
{
  const char * what () const throw ()
  {
    return "C++ Exception";
  }
};
 
int main()
{
  try
  {
    throw MyException();
  }
  catch(MyException& e)
  {
    std::cout << "MyException caught" << std::endl;
    std::cout << e.what() << std::endl;
  }
  catch(std::exception& e)
  {
    //其他的错误
  }
}

3.函数声明后面的异常说明——空异常说明

void no_problem() throw() 表示不抛出任何异常,空异常。

如果这个函数不会抛出异常最好应该是写上这个,如果不写的话就是代表函数可以抛出任意的异常,那么这样的话编译器可能对代码就不能做一定的优化了。

因为如果函数会抛出异常,编译器可能还是需要加一些额外的代码来保证这个异常处理能顺利执行。如果写了,那么编译器就直到肯定不会抛出异常了,就可以放心大胆的优化了。

(如果函数的实现和你声明的不一样的话,编译是可以通过的,就是运行的时候会出错。)

比如C++标准库中bad_alloc类中所有的成员函数都有空异常说明

C++标准保证,string析构函数其他任意标准库类析构函数一样,不抛出异常。

派生类虚函数的异常说明必须与对应的基类虚函数的异常说明同样严格,或者比后者更加严格。什么叫严格,不抛出任何异常就是最严格的。这样的话,我们写catch的时候就好写了,就按基类的来写就行了

异常说明也是函数类型的一部分,所以进行函数类型的变量的赋值(是重载函数的匹配的时候,也需要考虑到这个吧)。同样是异常说明可以不完全相同,但是需要源指针要至少或者更加严格比目标。

4.为异常安全做努力

什么叫异常安全,当异常发生时候:

1.不会出现资源泄露——应对办法之一,对资源进行资源管理类的封转。

2.不会出现数据败坏——比如出现野指针之类的

一个函数可以分成异常安全的函数,和异常不安全的函数。异常安全的函数又根据其保证的承诺程度分成:

1.基本保证:保证不会出现数据败坏。数据保持一种合法的状态。——基本的保证。

2.强烈保证:保证如果出现异常,数据恢复到出现异常之前的状态。——如果复杂度和效率允许,提供这种保证,有个copy-and-swap技术,

3.不抛出异常保证:保证不会出现异常的情况。

比如内置类型上的所有操作都是提供nothrow保证。(当然并不是说函数声明的时候nothrow,就代表真的不会,要看函数实现的,声明的时候只是给你一个承诺)

对于一个系统来说,只要有一个函数是异常不安全的,那么整个系统都是异常不安全的,不存在局部异常安全的说法。

一个例子:

之前的代码:异常不安全

修改以后的代码:强烈保证

5.在windows系统中,我们在开发应用程序时应该使用结构化异常还是C++异常?

结构化异常是操作系统所提供的便利,它在任何语言中都可以使用。

C++异常处理只有在编写C++代码时才可以使用。

如果开发C++应用程序,那就应该使用C++异常,而不是结构化异常。

理由是C++异常是语言的一部分,编译器知道什么是一个C++对象。

这也就意味着编译器会自动生成代码来调用C++对象的析构函数,保证对象的释放。

 


如何让标准C++异常的写法也能捕获结构化异常

结构化异常: 也叫“SEH异常”: windows下:用 __try...__except(过滤表达式) 捕获,还有终结处理块。

标准C++异常:用 try{}catch(...){} 捕获

一个函数中不能既有 标准C++异常捕获 也有 结构化异常捕获的写法。

那都想支持怎么办,

1)如下配置一下:

这样的话使用C++异常也能够捕获到SEH异常”。

这样无论是在EXE还是DLL中,try{}catch(...){}同样可以捕获Windows异常

2)_set_se_translator 函数——把SEH异常转换成C++异常

除了上面设置以外,还需要调用_set_se_translator 函数。

_set_se_translator | Microsoft Docs

函数定义:

_se_translator_function _set_se_translator(
    _se_translator_function seTransFunction
);

其中seTransFuncion的类型签名:

typedef void (*_se_translator_function)(unsigned int, struct _EXCEPTION_POINTERS* );

第一个参数是int,通过GetExceptionCode 宏获取的,具体参见GetExceptionCode macro - Win32 apps | Microsoft Docs,有哪些返回值。

第二个参数是EXCEPTION_POINTERS的指针,通过GetExceptionInformation 宏获得的。

这个函数的目的就是把SEH异常转换成C++异常。设置seTransFunction实现体中throw抛出一个C++异常:

_set_se_translator(throw_myexception);

//结构化转为标准c++异常
void throw_myexception(unsigned int code,_EXCEPTION_POINTERS * ptr)
{
 throw MyException(code,ptr);
}

所以,函数的实现体一般都是把throw一个异常类,throw MyException

一般是这么实现的,:

/************************************************************************/
/* 自定义异常类 主要是转换结构化异常到标准c++异常 易于捕获  
/************************************************************************/
#ifndef __ExceptionTransfer_H__
#define __ExceptionTransfer_H__
#include "stdafx.h"
class MyException
{
public:
 MyException(unsigned int code,_EXCEPTION_POINTERS * ptr):errcode(code),ptreception(ptr){}
 unsigned int errcode;
 _EXCEPTION_POINTERS *ptreception;
};
#endif

_set_se_translator这个函数只对当前线程有用,其它线程要想转换SEH的异常就必须调用_set_se_translator一次。

除了 _set_se_translator 之外,还有另外几个类似的函数,是特定的情况下的调用抛出异常的函数:

 _set_invalid_parameter_handler(throw_InValidParmerHandel); //捕获无效参数错误 ex:atoi(NULL)


 _set_purecall_handler(throw_PureCallHandel); // 就是捕获虚函数调用purecall的时候抛出的异常


 ::_set_new_handler(throw_Mybadlloc);// 就是new 的时候抛出的异常见new delete 那篇文章


 ::_set_new_mode(1);

对应的throw函数可以这么写:

void throw_InValidParmerHandel(const wchar_t * express, const wchar_t *function, const wchar_t *file, unsigned int line, uintptr_t p_ress)
{
 throw 234;
}

void throw_PureCallHandel(void)
{
 throw 123;
}

int throw_Mybadlloc( size_t requireSize )
{
 g_FatalLog.WriteStr( "分配内存失败");
 abort();
 return EXCEPTION_EXECUTE_HANDLER;
}


端口

以下描述均是8086cpu。

前面说过,接口卡和主板上,装有各种接口芯片。这些外设的接口芯片的内部有若干寄存器。cpu将这些寄存器当作端口来访问。

(端口就是在接口芯片上的寄存器。CPU把这些寄存器看成是端口。)

cpu 是 通过 接口芯片上的端口 来 和外部设备进行读写数据的。

in和out指令

我们之前说过数据可以在端口上。那么怎么访问端口上的数据呢,使用in 和 out 指令。

cpu对这些端口进行统一编址,建立统一的端口地址空间,每一个端口在地址空间中都有一个地址。即每一个端口都有一个端口号。比如CMOS RAM 芯片就有两个端口,分别是70h(数据端口)和71h(地址端口)号端口。

(比如读取CMOS RAM的2号单元:1)先将2送入端口70h,2)再从端口71h读出2号单元的内容 。比如60h就是键盘输入的端口号)

in al 20h 从20h端口读入一个数据

out 20h al 往20端口写入一个数据

注:在in 和 out 指令中,只能用ax 或 al 来存放从端口中读入的数据或要发送到端口中的数据。

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
### 回答1: STM32是一款嵌入式系统开发板,中断异常是其重要的应用场景之一。 中断是指在处理器执行期间,由硬件或软件请求处理器暂停当前程序执行,转而执行另一个程序(中断服务程序)的过程。STM32中断分为内部中断和外部中断两种类型。内部中断是由处理器内部的某些事件产生的,比如说系统时钟溢出、定时器计数溢出等。外部中断则是由外部设备或传感器引脚触发产生的,比如说按键、光电开关等。 异常是一种由于程序错误或硬件故障而造成的突发事件,它会导致程序崩溃或者执行意外的操作。STM32异常包括硬件异常和软件异常。硬件异常是由处理器硬件错误引起的,比如说非法指令、除数为0等。软件异常则是由程序错误引起的,比如说数组越界、空指针等。 STM32中断异常处理都需要在程序中进行相应的配置和处理,具体的实现方法需要参考STM32的开发文档。 ### 回答2: STM32是一款广泛应用于嵌入式系统的微控制器。其中断异常是其核心功能之一。 中断是当系统执行某个指令时,检测到外部或内部事件发生时暂停当前任务的功能。STM32中的中断中断控制器(NVIC)负责管理。它可以根据优先级来确定中断的触发顺序,并且可以配置中断向量表中每个中断的具体功能。中断可以分为外部中断和内部中断。外部中断通常由外部触发器(例如按键或传感器)引发,并通过外部中断线连接到STM32芯片。内部中断则是由芯片内部的某个事件触发,例如定时器溢出或串口收到数据。中断处理程序(IRQ Handler)是中断发生时要执行的特定功能代码。 异常则是指非正常情况下的事件,例如系统错误、无效指令或非法访问内存等。STM32中的异常分为两类:硬件异常和软件异常。硬件异常由处理器内部的硬件模块自动检测并触发,如非法指令或内存访问错误。软件异常是通过软件指令触发的,例如断言失败或手动触发。异常处理程序(Exception Handler)是在异常发生时要执行的代码。该代码可以进行错误处理、恢复系统状态并采取相应措施以避免进一步的错误。 中断异常的正确处理对于嵌入式系统的稳定性和可靠性非常重要。通过合理配置中断异常处理程序,可以实现对外部事件和系统错误的及时响应和处理,提高系统的实时性和可用性。 ### 回答3: STM32是一款高性能的嵌入式微控制器。它具备丰富的中断异常处理功能,以提高其实时性和可靠性。 中断是指在程序执行过程中,由硬件或软件触发的一种特殊事件,它会打断程序的正常顺序执行,转而去处理该事件。STM32中的中断分为两类:外部中断和内部中断。 外部中断是指来自外部设备的中断信号,比如按键的按下、定时器的溢出等。我们可以通过设置中断触发条件、中断优先级以及编写中断服务函数来处理外部中断。 内部中断包括软件中断和硬件异常。软件中断是由软件触发的,通常用于系统异常处理、任务切换等。硬件异常是由硬件产生的,比如除零、访问非法地址等错误。这些异常会导致程序中断执行,并跳转到相应的异常处理程序中进行错误处理。 为了实现中断异常的处理,STM32提供了一系列的中断异常处理机制。它包括中断向量表、中断控制器、优先级管理、中断使能和标志位管理等功能。 中断向量表用于存储中断服务函数的地址,当中断发生时,处理器会自动根据中断号在中断向量表中找到相应的中断服务函数进行调用。 中断控制器用于管理和控制中断,包括中断屏蔽、中断优先级设置以及中断请求与响应等。 优先级管理机制用于确定中断的处理顺序。STM32支持多级优先级,可以根据中断的重要程度和紧迫程度进行设置。 中断使能和标志位管理用于控制中断的使能和状态。通过设置相应的中断使能位和中断标志位,可以实现中断的触发和清除。 总的来说,STM32中断异常机制提供了一种强大而灵活的方式来实现实时和可靠的系统响应。合理利用中断异常功能,可以提高系统的性能和可靠性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值