先来说说异常和中断的区别。中断可在任何时候发生,与CPU正在执行什么指令无关,中断主要由I/O设备、处理器时钟或定时器等硬件引发,可以被允许或取消。而异常是由于CPU执行了某些指令引起的,可以包括存储器存取违规、除0或者特定调试指令等,内核也将系统服务视为异常。中断和异常更底层的区别是当广义上的中断(包括异常和硬件中断)发生时如果没有设置在服务寄存器(用命令号0xb向8259-1中断控制器0x20端口读出在服务寄存器1,用0xb向8259-2中断控制器的0xa0端口读出在服务寄存器2)相关的在服务位(每个在服务寄存器有8位,共对应IRQ 0-15)则为CPU的异常,否则为硬件中断。
下面是WINDOWS2000根据INTEL x86处理器的定义,将IDT中的前几项注册为对应的异常处理程序(不同的操作系统对此的实现标准是不一样的,这里给出的和其它一些资料不一样是因为这是windows的具体实现):
中断号
名字
原因
0x0
除法错误
1、DIV和IDIV指令除0
2、除法结果溢出
0x1
调试陷阱
1、EFLAG的TF位置位
2、执行到调试寄存器(DR0-DR4)设置的断点
3、执行INT 1指令
0x2
NMI中断
将CPU的NMI输入引脚置位(该异常为硬件发生非屏蔽中断而保留)
0x3
断点
执行INT 3指令
0x4
整数溢出
执行INTO指令且OF位置位
0x5
BOUND边界检查错误
BOUND指令比较的值在给定范围外
0x6
无效操作码
指令无法识别
0x7
协处理器不可用
1、CR0的EM位置位时执行任何协处理器指令
2、协处理器工作时执行了环境切换
0x8
双重异常
处理异常时发生另一个异常
0x9
协处理器段超限
浮点指令引用内存超过段尾
0xA
无效任务段
任务段包含的描述符无效(windows不
使用TSS进行环境切换,所以发生该异常说明有其它问题)
0xB
段不存在
被引用的段被换出内存
0xC
堆栈错误
1、被引用内存超出堆栈段限制
2、加载入SS寄存器的描述符的present位置0
0xD
一般保护性错误
所有其它异常处理例程无法处理的异常
0xE
页面错误
1、访问的地址未被换入内存
2、访问操作违反页保护规则
0x10
协处理器出错
CR0的EM位置位时执行WAIT或ESCape指令
0x11
对齐检查错误
对齐检查开启时(EFLAG对齐位置位)访问未对齐数据
其它异常还包括获取系统启动时间服务int 0x2a、用户回调int 0x2b、系统服务int 0x2e、调试服务int 0x2d等系统用来实现自己功能的部分,都是通过异常的机制,触发方式就是执行相应的int指令。
这里给出几个异常处理中重要的结构:
陷阱帧TrapFrame结构(后面提到的异常帧ExceptionFrame结构其实也是一个KTRAP_FRAME结构):
typedef struct _KTRAP_FRAME {
ULONG
DbgEbp;
ULONG
DbgEip;
ULONG
DbgArgMark;
ULONG
DbgArgPointer;
ULONG
TempSegCs;
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;
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;
环境Context结构:
typedef struct _CONTEXT {
ULONG ContextFlags;
ULONG
Dr0;
ULONG
Dr1;
ULONG
Dr2;
ULONG
Dr3;
ULONG
Dr6;
ULONG
Dr7;
FLOATING_SAVE_AREA FloatSave;
ULONG
SegGs;
ULONG
SegFs;
ULONG
SegEs;
ULONG
SegDs;
ULONG
Edi;
ULONG
Esi;
ULONG
Ebx;
ULONG
Edx;
ULONG
Ecx;
ULONG
Eax;
ULONG
Ebp;
ULONG
Eip;
ULONG
SegCs;
ULONG
EFlags;
ULONG
Esp;
ULONG
SegSs;
UCHAR
ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;
异常记录ExceptionRecord结构:
typedef struct _EXCEPTION_RECORD {
NTSTATUS ExceptionCode;
ULONG ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
ULONG NumberParameters;
ULONG_PTR ExceptionInformatio[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;
当发生异常后,CPU记录当前各寄存器状态并在内核堆栈中建立陷阱帧TrapFrame,然后将控制交给对应异常的陷阱处理程序。当陷阱处理程序能处理异常时,比如缺页时通过调页程序MmAccessFault将页换入物理内存后通过iret返回发生异常的地方。但大多数无法处理异常,这时先是调用CommonDispatchException在内核堆栈中建立异常记录ExceptionRecord和异常帧ExceptionFrame。ExceptionRecord很重要,它记录了异常代码、异常地址以及一些其它附加的参数。然后调用KiDispatchException进行异常的分派。这个函数是WINDOWS下异常处理的核心函数,负责异常的分派处理。
KiDispatchException的处理流程(每当异常被某个例程处理时处理的例程将返回TRUE到上一个例程,未处理则返回FALSE。当任何一个例程处理了异常返回TRUE时,则KiDispatchException正常返回):
在进行用户态内核态的异常的分派前,先判断异常是否来自用户模式,是的话将Context.ContextFlags(这时候Context结构还刚初始化完,还未赋初值) or上CONEXT_FLOATING_POINT,意味着对来自用户模式的异常总是尝试分派浮点状态,这样可以允许异常处理程序或调试器检查和修改协处理器的状态。然后从陷阱帧中取出寄存器值填入Context结构,并判断是否是断点异常(int 0x3和int 0x2d),如果是的话先将Context.Eip减一使它指向int 0x3指令(无论是由int 0x3还是由int 0x2d引起的异常,因为前面的陷阱处理程序里已经改变过TrapFrame里面的Eip了)。然后判断异常是发生于内核模式还是用户模式,根据不同模式而采取不同处理过程。
如果异常发生于内核模式,会给予内核调试器第一次机会和第二次机会处理异常。当异常被处理后就将设置好陷阱帧并返回到陷阱处理程序,在那里iret返回发生异常的地方继续执行。
内核模式异常处理流程为:
(第一次机会)判断KiDebugRoutine是否为空,不为空就将Context、陷阱帧、异常记录、异常帧、发生异常的模式等压入栈并将控制交给KiDebugRoutine。
若KiDebugRoutine为空(正常的系统这里不为空。正常启动的系统KiDebugRoutine为KdpStub,在Boot.ini里加上/DEBUG启动的系统的KiDebugRoutine为KdpTrap。如果这里为空的话会因为处理不了DbgPrint这类int 0x2d产生的异常而导致系统崩溃)或者KiDebugRoutine未处理异常,则将Context结构和异常记录ExceptionRecord压栈并调用内核模式的RtlDispatchException在内核堆栈中查找基于帧的异常处理例程。
RtlDispatchException调用RtlpGetRegistrationHead从fs:[0](0xffdff000)处获取当前线程异常处理链表指针,并调用RtlpGetStackLimits从0xffdff004和0xffdff008取出当前线程堆栈底和顶。然后开始由异常处理链表指针遍历链表查找异常处理例程(若在XP和2003下先处理VEH再处理SEH),其实这就是SEH,只是和用户态有一点不同是既没有顶层异常处理例程(TOP LEVEL SEH)也没有默认异常处理例程。然后对每个当前异常处理链表指针检查判断堆栈是否有效(是否超出了堆栈范围或者未对齐)及堆栈是否是DPC堆栈。若0xffdff80c处DpcRoutineActive为TRUE且堆栈顶和底在0xffdff81c处取出的DpcStack到DpcStack-0x3000(一个内核堆栈大小),若是则更新堆栈顶和底为DpcStack和DpcStack-0x3000并继续处理,否则将异常记录结构里的异常标志ExceptionRecord.ExceptionFlags设置EXCEPTION_STACK_INVALID表示为无效堆栈并返回FALSE。
调用异常处理链表上的异常处理例程之前会在异常处理例程链表上插入一个新的节点,对应的异常处理例程是用来处理嵌套异常,也就是在处理异常时发生另一个异常。处理后
RtlDispatchException判断异常处理例程的返回值:
若为ExceptionContinueExecution,若异常标志ExceptionRecord.ExceptionFlags未设置EXCEPTION_NONCONTINUABLE不可恢复执行,则返回TRUE到上一层,否则在做了一些工作后调用RtlRaiseException进入到KiDispatchException的第二次机会处理部分。
若为ExceptionContinueSearch,则继续查找异常处理例程。
若为ExceptionNestedException,嵌套异常。保留当前异常处理链表指针为内层异常处理链表并继续查找异常处理例程。当发现当前异常处理链表地址大于保留的内层异常处理链表时,表示当前的异常处理链表比保留的更内层(因为堆栈是由高向低扩展的,地址越高则入栈越早,表示更内层),则将其值赋予内层异常处理链表指针,除了第一次赋初值外发生修改保留的内层