首先讲下硬中断和软中断的区别
硬中断:硬中断是由硬件产生的,比如,像磁盘,网卡,键盘,时钟等。每个设备或设备集都有它自己的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来恢复中断。
上述都是硬件异常,
软件异常是由操作系统或者应用程序产生的。其主要包含
这些异常的最终实现都是基于用户态的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
#include
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的处理,然后进行对于不同模式的异常分发。
上述过程只要在任何一步被handler或者调试器处理,异常处理就会结束。