浅谈中断与异常

中断

首先讲下硬中断和软中断的区别

硬中断:硬中断是由硬件产生的,比如,像磁盘,网卡,键盘,时钟等。每个设备或设备集都有它自己的IRQ(中断请求)。基于IRQ,CPU可以将相应的请求分发到对应的硬件驱动上(注:硬件驱动通常是内核中的一个子程序,而不是一个独立的进程)。

软中断:
1. 软中断的处理非常像硬中断。然而,它们仅仅是由当前正在运行的进程所产生的。
2. 通常,软中断是一些对I/O的请求。这些请求会调用内核中可以调度I/O发生的程序。对于某些设备,I/O请求需要被立即处理,而磁盘I/O请求通常可以排队并且可以稍后处理。根据I/O模型的不同,进程或许会被挂起直到I/O完成,此时内核调度器就会选择另一个进程去运行。I/O可以在进程之间产生并且调度过程通常和磁盘I/O的方式是相同。
3. 软中断仅与内核相联系。而内核主要负责对需要运行的任何其他的进程进行调度。一些内核允许设备驱动的一些部分存在于用户空间,并且当需要的时候内核也会调度这个进程去运行。
4. 软中断并不会直接中断CPU。也只有当前正在运行的代码(或进程)才会产生软中断。这种中断是一种需要内核为正在运行的进程去做一些事情(通常为I/O)的请求。

具体更详细的解释参考这位大佬的博客:https://www.cnblogs.com/charlesblc/p/6263208.html,讲的很详细
例如在程序中写了 int xx这样的语句,会调用内核中相应的代码,但是它并不中断CPU,我们可以看一下内核中的中断例程,当有中断发生的时候,程序是在IDT中寻找处理函数的,当有中断或异常发生的时候(这里把异常也视为中断的一种),根据中断号码来选择中断处理例程的,寻找的地址是IDT的地址加上8 *中断号。
在这里插入图片描述
在这里插入图片描述
可以看到上述的一般都是KiTrap函数,x86不使用中断门来处理东西,所以KiTrap系列函数最常用的方式

硬件异常

异常是CPU内部产生的中断,即在CPU执行特定指令的时候出现的非法情况,如除数为0等等,所以不可能在执行指令期间发生异常,只会在执行一条指令后有可能发生,所以也称同步中断
而中断则是一种异步的,它与特定的进程是无关的。

异常主要分三种,trap(陷入/陷阱,总感觉翻译的怪怪的),fault(错误),abort(终止)。前两种是可恢复的,最后一种不可恢复。trap与fault的最大不同就是发生异常时候保留的EIP不同,trap保留的EIP是发生异常时候的下一条指令的地址,而fault保存的EIP是发生异常的本条指令。举个浅显的例子,fault常见的例如od中我们F2设置普通断点的时候,od会将我们选中的那条指令替换成0xCC,即int 3指令,当执行到这里的时候,cpu会处理这条int 3指令,然后发生一个异常,这个时候就把控制权交给了我们的od,然后od等待用户的操作,当执行的时候,将我们下断点的那个地址重新替换成原本的指令。然后这条指令就相当于“下一条指令”,接着我们单步执行就会执行我们原本的那条指令了。而对于fault的话很常见的就是缺页中断,当发生缺页中断的时候,操作系统会尝试就该页载入内存,然后重新执行我们那条读取/写入页的指令。abort则是为了处理严重的硬件错误等,这类异常不会回复再执行,会强制性退出。

异常和中断会被写进一张叫做IDT的表里,我们可以通过idtr寄存器寻找它,它是一个48位的寄存器,高32位表示IDT的基址,低16位代表长度。IDT里的每一个中断例程都是八字节大小,具体的结构参见我的中断门那一讲。但是IDT里并不只是中断例程,它包括三种不同的描述符(即类似于功能的不同),任务门,中断门,陷阱门。

IDT被设置是在操作系统启动后的,有个名叫Winload的程序会首先cli,屏蔽外部中断,然后利用指令lidt,将信息告诉CPU,接着将控制权交给入口函数,调用sidt保存下来idt的信息,然后其他的处理器也会进行类似的操作,修改和复制idt,最后调用sti来恢复中断。

上述都是硬件异常,

软件异常

软件异常是由操作系统或者应用程序产生的。其主要包含

  1. 操作系统事先为我们准备的异常处理过程
  2. 我们自己的异常处理(try catch)

这些异常的最终实现都是基于用户态的RaiseException内核的NtRaiseException建立起来的。
RaiseException的原型如下:

void WINAPI RaiseException(
  _In_       DWORD     dwExceptionCode,
  _In_       DWORD     dwExceptionFlags,
  _In_       DWORD     nNumberOfArguments,
  _In_ const ULONG_PTR *lpArguments
);

ExceptionCode是异常状态码,用于表示是什么原因导致异常,由操作系统指定,也可以由用户程序指定。ExceptionFlag是状态属性

抄一个微软官方的例子看下:

#include "stdafx.h"
#include <stdio.h>
#include <Windows.h>


DWORD FilterFunction()
{
	printf("1 ");                     // printed first 
	return EXCEPTION_EXECUTE_HANDLER;
}

BOOL CheckForDebugger()
{
	__try
	{
		DebugBreak();
	}
	__except (GetExceptionCode() == EXCEPTION_BREAKPOINT ?
	EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
	{
		// No debugger is attached, so return FALSE 
		// and continue.
		return FALSE;
	}
	return TRUE;
}
VOID main(VOID)
{
	BOOL bRet = CheckForDebugger();
	printf(bRet ? "true" : "false");
}

在这里插入图片描述到这里会进行DebugBreak,执行完后会触发一个异常。进入后会引发一个int3断点异常,然后将控制权交给od,然后我们可以选择处理,然后会true,不然根据默认情况,是直接false。

其内部会将异常的相关信息传入一个维护异常的结构,叫做_EXCEPTION_RECORD,然后再去调用RtlRaiseException。并且一般说来,各个异常处理函数除了针对本异常的特殊处理外,通常会将异常信息进行封装。封装的主要有两部分,一部分就是异常记录

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

一部分的ExceptionCode
在这里插入图片描述
ExceptionRecord是指向下一个异常的指针。ExceptionRecord是异常发生的地址。NumberParameters是参数的个数。Exception的个数与NumberParameters有关系,最大应不超过EXCEPTION_MAXIMUM_PARAMETERS。

另一部分被封装的内容称为陷阱帧(Trap Frame)。主要记录异常发生时候的线程状态。x86平台是这样定义的。

typedef struct _KTRAP_FRAME
{
	//这四项仅为调试系统服务
     ULONG DbgEbp;//拷贝的ebp
     ULONG DbgEip;//拷贝的eip
     ULONG DbgArgMark;
     ULONG DbgArgPointer;

	 //用于调整栈帧时所需要的
     WORD TempSegCs;
     UCHAR Logging;
     UCHAR Reserved;
     ULONG TempEsp;
	
	 //调试寄存器
     ULONG Dr0;
     ULONG Dr1;
     ULONG Dr2;
     ULONG Dr3;
     ULONG Dr6;
     ULONG Dr7;
     	
     //段寄存器
     ULONG SegGs;
     ULONG SegEs;
     ULONG SegDs;

	 //常规寄存器
     ULONG Edx;
     ULONG Ecx;
     ULONG Eax;

	 //
     ULONG PreviousPreviousMode;//异常发生时所在的层
     PEXCEPTION_REGISTRATION_RECORD ExceptionList;//异常链
     ULONG SegFs;//Fs寄存器

	//常规寄存器
     ULONG Edi;
     ULONG Esi;
     ULONG Ebx;
     ULONG Ebp;

	//控制寄存器
     ULONG ErrCode;
     ULONG Eip;
     ULONG SegCs;
     ULONG EFlags;

	
     ULONG HardwareEsp;
     ULONG HardwareSegSs;
     ULONG V86Es;
     ULONG V86Ds;
     ULONG V86Fs;
     ULONG V86Gs;
} KTRAP_FRAME, *PKTRAP_FRAME;

该结构一般是内核使用的,当将控制权交给用户的时候,会将上述的数据结构,转换成一个名为CONTEXT的结构。

typedef struct _CONTEXT {
            ULONG ContextFlags;		//这里是控制flag,与要给用户哪些寄存器有关

			//调试寄存器,当ContextFlags包含CONTEXT_DEBUG_REGISTERS
            ULONG   Dr0;
            ULONG   Dr1;
            ULONG   Dr2;
            ULONG   Dr3;
            ULONG   Dr6;
            ULONG   Dr7;
            
            //浮点运算器 当包含CONTEXT_FLOARTING_POINT时有效
            FLOATING_SAVE_AREA FloatSave;

			//包含CONTEXT_SEGMENTS时有效
            ULONG   SegGs;
            ULONG   SegFs;
            ULONG   SegEs;
            ULONG   SegDs;

			//包含CONTEXT_INTEGERS
            ULONG   Edi;
            ULONG   Esi;
            ULONG   Ebx;
            ULONG   Edx;
            ULONG   Ecx;
            ULONG   Eax;

			//包含CONTEXT_CONTROL
            ULONG   Ebp;
            ULONG   Eip;
            ULONG   SegCs;   
            ULONG   EFlags; 
            ULONG   Esp;
            ULONG   SegSs;
			
			//包含CONTEXT_EXTENDED_REGISTERS
            UCHAR   ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
    } CONTEXT;

其定义在WinNT头里:
在这里插入图片描述
包装完毕后会进一步调用系统内核的异常处理函数KiDispatchException函数来分发处理异常。KiDispatchException是处理的主要核心,其它函数只是对ExceptionRecord和TrapFrame的一种封装然后传给KiDispatchException的过程。
原型如下:

VOID
KiDispatchException (
    IN PEXCEPTION_RECORD ExceptionRecord,		//异常处理 封装了本次异常的状态码,flag,指针,参数,地址
    IN PKEXCEPTION_FRAME ExceptionFrame,		//总为NULL
    IN PKTRAP_FRAME TrapFrame,					//陷阱帧
    IN KPROCESSOR_MODE PreviousMode,			//发生异常时CPU处于什么模式
    IN BOOLEAN FirstChance						//是否第一次处理异常
    )

KiDispatchException主要是的功能如下
预处理:
首先设置相应的context_flag,对于用户程序且内核调试被启用,若额外复制一个CONTEXT_FLOATING_POINT的flag
然后用TrapFrame根据flag填充context结构
再分发之前对于STATUS_BREAKPOINT会进行eip-1的处理,然后进行对于不同模式的异常分发。

  1. 若处于kernelMode
    1.1若是第一次处理该异常,并且存在内核调试器,即KiDebugRoutine存在(此时存在KdpTrap),则将参数分发给KiDebugRoutine,若返回false(也就是内核调试器没有处理该异常),则交给RtlDispatchException处理,RtlDispatchException会试图寻找已注册的SEH,如果处理了返回true,否则返回false继续往下找。
    1.2然后系统会给内核调试器第二次机会去处理异常。如果选择了不处理,则会调用KeDebugCheckEx,表明KERNEL_MODE_EXCEPTION_NOT_HANDLED。
  2. 若处于userMode
    2.1若是第一次处理该异常,并且内核调试器存在,而且进程存在一个调试端口可用,则先分发给内核调试器处理,若不处理发往调试子系统,调试子系统发送给用户态调试器。如果未处理异常则继续。接着首先判断ss段寄存器是否为32位,若不是则没有必要继续,抛出一个STATUS_ACCESS_VIOLATION的异常。若是32位,则继续,判断userStack是否可写,若可写,则将context复制到用户栈上。类似的,会将ExceptionRecord,context的地址,ExceptionRecord的地址依次复制到栈上。然后返回用户层执行执行KiUserExceptionDispatcher,此函数会调用RtlUserExceptionDispatcher,首先遍历VEH,接着遍历SEH(SEH部分包括自己定义的SetUnhandledExceptionFilter,但调试器若存在,是会被交给调试器处理的在链表的最末尾是UnhandledExceptionFilter)。若为处理则继续。
    2.2此时是第二次处理异常,尝试再次发给调试端口,然后发往用户态的调试器,若仍然不处理,则将异常发往异常端口,异常端口通常由子系统csrss.exe监听,通常的做法是显示一个“应用程序错误"如果失败则调用ZwTerminateProcess来结束进程

上述过程只要在任何一步被handler或者调试器处理,异常处理就会结束。

猜你喜欢

转载自blog.csdn.net/qq_40890756/article/details/89852126