老铁们,大家好,相信还有很多朋友对于从操作系统的角度讨论中断和异常机制(Windows)和的相关问题不太懂,没关系,今天就由我来为大家分享分享从操作系统的角度讨论中断和异常机制(Windows)以及的问题,文章篇幅可能偏长,希望可以帮助到大家,下面一起来看看吧!
IDT的位置和长度由CPU的IDTR描述。 IDTR共有48位,高32位是IDT的基地址,低16位是IDT的长度(限制)。 LIDT(Load IDT)指令用于将操作数指定的基址和长度加载到IDTR中,即重写IDTR的内容。 SIDT(Store IDT)指令用于将IDTR的内容写入内存变量,即读取IDTR的内容。 LIDT 和SIDT 指令只能在高特权级别(Ring 0)的实模式或保护模式下执行。在调试内核时,可以使用rigtr 和rigtl 命令观察IDTR 的内容(第1 卷第2.6.2 节)。
在Windows操作系统中,IDT的初始化过程大致是这样的。 IDT的初始建立和初始化是由Windows系统加载器(NTLDR或WinLoad)以实模式完成的。准备好内存块后,加载器首先执行CLI指令关闭中断处理,然后执行LIDT指令将IDT的位置和长度信息加载到CPU中。然后,加载程序将CPU从实模式切换到保护模式,并将执行权转移给NT内核的入口函数KiSystemStartup。接下来,内核中的处理器初始化函数会通过SIDT指令获取IDT信息,对其进行必要的调整,然后以参数的形式传递给KiInitializePcr函数,KiInitializePcr函数将其记录到描述的基本数据区PCR中处理器(处理器控制区域)和Prcb(处理器控制块)。
在内核调试会话中,可以使用!PCR命令来观察CPU的PCR内容。清单11-1显示了Windows Vista系统中CPU 0的PCR内容。
清单11-1 Windows Vista系统中0号CPU的PCR内容
kd!pcrKPCR for Processor 0 at 81969a00: //KPCR 结构体的线性内存地址Major 1 Minor 1 //KPCR 结构体的主版本号和子版本号NtTib.ExceptionList: 9f1d9644 //异常处理注册列表[.] //省略几行关于NTTIB信息SelfPcr: 81969a00 //该结构体的起始地址Prcb: 81969b20 //KPRCB结构体的地址Irql: 0000001f //CPU中断请求级别(IRQL) IRR: 00000000 //IDR: ffff20f0 //中断模式: 00000000 //IDT: 834da400 //IDT GDT: 的基址834da000 //GDT 的基址TSS: 8013e000 //任务状态段(TSS)的地址CurrentThread: 84af6270 //当前正在执行的线程,ETHREAD 地址NextThread: 00000000 //下一个要执行的线程IdleThread: 8196 cdc0 //IDLE 线程的ETHREAD 地址内核数据结构KPCR 描述了PCR 内存区域的布局,因此也可以使用dt 命令来观察PCR,例如kddt nt!_KPCR 81969a00。
11.1.2 门描述符
IDT的每个条目都是所谓的门描述符结构。之所以这样称呼,是因为IDT条目的基本目的是引导CPU从一个空间到另一个空间执行。每个表项似乎都是从一个空间到另一个空间的大门。 CPU在经过这扇门时会做必要的安全检查和准备。
IDT 中可以包含以下三种类型的门描述符。
(1)任务门描述符:用于任务切换,其中包含用于选择任务状态段(TSS)的段选择器。可以使用JMP或CALL指令通过任务门切换到任务门指向的任务。当CPU因中断或异常而转移到任务门时,也会切换到指定的任务。
(2)中断门描述符:用于描述中断处理例程的入口。
(3)陷阱门描述符:用于描述异常处理例程的入口。
图11-1描述了上述三个门描述符的内容布局。
图11-1 IDT中三个门描述符的内容布局
从图11-1可以看出,三个门描述符的格式非常相似,并且有很多共同的字段。其中,DPL表示描述符优先级(descriptor previlege level),用于优先级控制,P是段存在标志。段选择器用于选择一个段描述符(位于LDT或GDT中,选择器的格式请参考本书第1卷2.6.3节)。偏移部分用于指定段中的偏移量。两者一起定义了一个精确的内存位置。对于中断门和陷阱门,它们指定中断或异常处理例程的地址;对于任务门,它们指定任务状态段的内存地址。
系统通过门描述符的类型字段,即高4字节的612位来区分描述符的类型。例如,任务门的类型为0b00101(b代表二进制数),中断门的类型为0b0D110,其中D位用于指示描述的是16位门(0)还是a 32位门(1),陷阱门的类型为0b0D111。
11.1.3 执行中断和异常处理函数
我们来看看当中断或异常发生时CPU是如何通过IDT找到并执行处理函数的。首先,CPU会根据其向量号和IDTR中的IDT基地址信息找到对应的门描述符。然后确定门描述符的类型。如果是任务描述符,那么CPU会进行硬件任务切换,切换到这个描述符定义的线程;如果是陷阱描述符或者中断描述符,那么CPU会在当前任务中切换到当前任务。描述符描述的处理例程在上下文中被调用。下面分别讨论它们。
我们先看一下任务门的情况。简单来说,任务门描述了一个TSS。 CPU要做的就是切换到TSS代表的线程,然后开始执行该线程。 TSS是用于保存任务信息的内存区域,其格式由CPU定义。图11-2 显示了IA-32 CPU 的TSS 格式。由此我们可以看出,TSS包含了任务的关键上下文信息,例如段寄存器、通用寄存器和控制寄存器。特别值得注意的是底部的SS0~SS2 和ESP0~ESP2 字段,它们记录任务的状态。当以不同优先级执行时应该使用堆栈。 SSx用于选择堆栈所在的段。 ESPx 是堆栈指针值。
CPU通过任务门的段选择器找到TSS描述符后,会执行一系列检查动作,比如确保TSS描述符中的存在标志为1、边界值应大于0x67、B (忙)标志不为1等。所有检查通过后,CPU将当前任务的状态保存到当前任务的TSS中。然后将TSS描述符中的B标志设置为1。接下来CPU将新任务的段选择器(相当于与门描述符中的段选择器)加载到TR寄存器中,然后加载新任务的寄存器信息任务写入物理寄存器。最后,CPU开始执行新任务。
图11-2 32 位任务状态段(TSS)
我们通过一个小实验来加深大家的理解。首先,在调试Windows Vista的内核调试会话中,通过ridtr命令获取系统IDT的基地址。
kdr idtridtr=834da400 由于双错误异常(Double Failure,#DF)通常使用任务门来处理,因此我们观察该异常对应的IDT 条目。由于#DF异常的向量号为8,并且每个IDT项的长度为8个字节,因此我们可以使用以下命令来显示第8号IDT项的内容。
kddb 834da400+8*8 l8834da440 00 00 50 00 00 85 00 00 .P.第2、3个字节(从0开始,下同)组成的WORD是段选择器,即0x0050。字节5 (0x85) 是P 标志(为1)、DPL (0b00) 和类型(0b00101)。
接下来,使用dg命令显示段选择器指向的段描述符。
kddg 50 P Si Gr Pr LoSel 基本限制类型l ze an es ng 标志---- -------- -------- ---------- ---- ------ - -- - - -- -- -----------0050 81967000 00000068 TSS32 Avl 0 Nb By P Nl 00000089 也就是说,TSS 的基地址为0x81967000,长度为0x68 字节(Gran 位指示By 即Byte )。 Type字段显示该段的类型为32位TSS(TSS32),其状态为Available,而不是Busy。
至此,我们知道#DF异常对应的门描述符指向的TSS是从内存地址0x81967000开始的0x68字节。该TSS 的内容可以使用内存监视命令来显示(清单11-2)。
清单11-2 TSS的内容
kddd 8196700081967000 00000000 81964000 00000010 0000000081967010 00000000 00000000 00000000 0012200081967020 8193f0a0 00000 000 00000000 0000000081967030 00000000 00000000 81964000 0000000081967040 00000000 00000000 00000023 0000000881967050 00000 0 10 00000023 00000030 0000000081967060 00000000 20ac0000 00000000 8196400081967070 00000010 00000000 00000000 00000000 参见清单1 1-2,从上到下,81964000优先级为0时为优先级0执行时的堆栈指针,00000010为优先级0执行时的堆栈选择器,00122000为本任务的页目录基址寄存器(PDBR、CR3)的值,8193f0a0为程序指针寄存器(EIP)的值,当CPU切换到该任务时,从这里开始执行。接下来是标志寄存器(EFLAGS)和通用寄存器的值。偏移0x48字节处的0x23是ES寄存器的值,相邻的00000008是CS寄存器的值,也就是本次任务的代码段的选择器。然后是SS寄存器的值,它是堆栈段的选择器,然后是DS、FS和GS寄存器的值(0x23、0x30和0)。偏移0x64字节处的20ac0000是TSS的最后4个字节,其最低位是T标志(0),即我们在第1卷4.3.3节中介绍的TSS中的陷阱标志。使用高16字节定位IO映射区基地址相对于TSS基地址的偏移地址。
使用ln命令观察EIP的值对应的是内核函数KiTrap08。
kdln 8193f0a0 (8193f0a0) nt!KiTrap08 | kdln 8193f0a0 (8193f0a0) (8193f118) nt!Dr_kit9_aExact matches: nt!KiTrap08=也就是说,当#DF异常发生时,CPU会切换到上面TSS描述的线程,然后在这个线程环境中执行KiTrap08函数。之所以我们需要切换到新的线程,而不是像其他异常一样在原来的线程中处理,是因为#DF异常指的是处理异常时发生的另一个异常,这可能意味着原来的线程环境已经是了不再可靠,所以需要切换到新的线程执行。
类似地,代表紧急任务的不可屏蔽中断(NMI)是使用任务门机制来处理的。最后需要注意的是,由于x64架构不支持基于硬件的任务切换,因此IDT中不再存在任务门。
大多数中断和异常都是使用中断门或陷阱门来处理的。下面我们就来看看这两种情况。
首先,CPU会根据门描述符中的段选择器来定位段描述符,然后进行一系列的检查。如果检查通过,CPU将判断是否需要切换堆栈。如果目标代码段的特权级高于当前特权级(级别值较小),则CPU需要通过读取该代码段的段选择器(SS)和堆栈指针(ESP)来切换堆栈。来自当前任务的TSS 的新堆栈。 )并将其加载到SS 和ESP 寄存器中。然后,CPU会将被中断进程(旧)的堆栈段选择器(SS)和堆栈指针(ESP)压入新堆栈。接下来,CPU将执行以下两个操作。
(1) 将EFLAGS、CS、EIP的指针压入堆栈。指向CS 和EIP 的指针表示CPU 在进入处理例程之前执行代码的位置。
(2)如果发生异常,并且该异常有错误代码(参见本书第1卷第3.3.2节),则该错误代码也被压入堆栈。
如果处理例程所在代码段的特权级与当前特权级相同,则CPU不需要进行堆栈切换,但仍然要执行上述两步操作。
11.1.4 IDT一览
使用WinDBG 的!idt 扩展命令列出IDT 中的每一项。然而,该命令进行了大量翻译,并且不显示门描述符的原始格式。
lkd!idt -a Dumping IDT:00: 804dbe13 nt!KiTrap00 //异常0,即除以001: 804dbf6b nt!KiTrap0102: Task Selector=0x0058 //NMI的门描述符显示TSS的选择器03: 804d c 2bd nt!KiTrap03 表11 -1列出了典型Windows系统的IDT设置。对于不同的Windows版本或不同硬件配置的系统,某些条目可能会有所不同,但大多数条目是一致的。
表11-1 IDT设置概述(略)
在Windows XP系统中,处理机器检查异常(#MC)的入口18是一个任务门描述符,指向一个单独的TSS,对应的处理函数是hal模块中的HalpMcaExceptionHandlerWrapper。
11.2 异常的描述和登记
为了更好地管理异常,Windows系统定义了一种特殊的数据结构来描述异常,并定义了一系列代码来识别典型的异常。
在操作系统层面,除了CPU产生的异常外,还有通过软件模拟的异常,例如调用RaiseException API产生的异常、使用编程语言的throw关键字抛出的异常等。为了书写方便,我们将前一类称为CPU异常(或硬件异常),将后一类称为软件异常。 Windows使用统一的方式来描述和分发这两类异常。本节介绍异常的描述方法,11.3节将介绍异常的分发过程。
11.2.1 EXCEPTION_RECORD结构
Windows 系统使用EXCEPTION_RECORD 结构来描述异常。清单11-3 给出了该结构的定义。
清单11-3 EXCEPTION_RECORD 结构
typedef struct _EXCEPTION_RECORD { DWORD 异常代码; //异常代码DWORD ExceptionFlags; //异常标志struct _EXCEPTION_RECORD* ExceptionRecord; //另一个相关异常PVOID ExceptionAddress; //异常发生地址DWORD NumberParameters; //参数数组中的元素数量ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; //参数数组} EXCEPTION_RECORD, *PEXCEPTION_RECORD;其中ExceptionCode是异常代码,它是一个32位整数。其格式是Windows系统的状态码格式。 NtStatus.h 包含所有已定义的状态。代码,在WinBase.h中可以看到,异常代码只是状态代码的别名,例如:
#define EXCEPTION_BREAKPOINT STATUS_BREAKPOINT #define EXCEPTION_SINGLE_STEP STATUS_SINGLE_STEP 表11-2 列出了用于异常代码的常见状态代码。
该字段用于记录异常标志。它的每一位代表一个标志。当前定义的标志位如下。
(1) EH_NONCONTINUABLE (1),该异常无法恢复,继续执行。
(2) EH_UNWINDING (2),当由于执行堆栈展开而调用异常处理函数时,设置该标志。
(3)EH_EXIT_UNWIND(4),也是用于栈扩展,很少使用。
(4) EH_STACK_INVALID (8),当检测到堆栈错误时设置该标志。
(5) EH_NESTED_CALL (0x10),用于识别嵌入异常(第24章)。
EH_NONCONTINUABLE 位用于指示异常是否可以恢复并继续执行。如果尝试恢复运行无法继续的异常,将会导致EXCEPTION_NONCONTINUABLE_EXCEPTION 异常。
ExceptionRecord指针指向与该异常相关的另一个异常记录。如果没有相关异常,则该指针为空。
表11-2:异常代码的状态代码(略)
ExceptionAddress字段用于记录异常地址。对于硬件异常,其值可能是引起异常的指令的地址,也可能是引起异常的下一条指令的地址,具体取决于异常类型。例如,非法访问异常(EXCEPTION_ACCESS_VIOLATION)就属于错误(Fault)类异常,ExceptionAddress的值为引起该异常的指令的地址。数据断点触发的调试异常属于陷阱异常,ExceptionAddress的值为引起异常的下一条指令的地址。
NumberParameters 是附加参数的数量,即ExceptionInformation 数组中包含的有效参数的数量。该结构允许存储最多15 个附加参数。
11.2.2 登记CPU异常
对于CPU异常,KiTrapXX例程完成针对该异常的特殊动作后,通常会调用CommonDispatchException函数,并通过寄存器将以下信息传递给该函数。
(1) 将唯一标识异常的异常代码(表11-2)放入EAX 寄存器中。
(2) 将引起异常的指令地址放入EBX寄存器中。
(3) 将其他信息作为附带参数(最多3 个)分别放入EDX(参数1)、ESI(参数2)和EDI(参数3)寄存器,并将参数个数放入ECX 寄存器。
CommonDispatchException调用后,会在栈上分配一个EXCEPTION_RECORD结构体,并在该结构体中存储上述异常信息。准备好该结构后,它会调用内核中的KiDispatchException 函数来调度异常。
11.2.3 登记软件异常
我们看一下软件异常的生成和注册过程。简单来说,软件异常是通过直接或间接调用内核服务NtRaiseException产生的。
NTSTATUS NtRaiseException (IN PEXCEPTION_RECORD ExceptionRecord, IN PCONTEXT ContextRecord, IN BOOLEAN FirstChance) 用户模式下的程序可以通过RaiseException() API 调用此内核服务。 RaiseException API 是KERNEL32.DLL 导出的API,它允许应用程序生成“自定义”异常。其原型如下。
void RaiseException( DWORD , DWORD , DWORD , const DWORD* );其中是异常代码,可以是表11-2中的代码,也可以是应用程序自己定义的代码。以及用于定义异常的常量,相当于EXCEPTION_RECORD 结构中的和。其实RaiseException的实现也很简单。它只是将参数放入EXCEPTION_RECORD 中,然后调用NTDLL.DLL 中的RtlRaiseException()。 RtlRaiseException会将当前执行上下文(通用寄存器等)放入CONTEXT结构体中,然后通过NTDLL.DLL中的系统服务调用机制来调用内核中的NtRaiseException。
NtRaiseException 在内部调用另一个内核函数KiRaiseException。
NTSTATUS KiRaiseException (IN PEXCEPTION_RECORD ExceptionRecord, IN PCONTEXT ContextRecord, IN PKEXCEPTION_FRAME ExceptionFrame, IN PKTRAP_FRAME TrapFrame, IN BOOLEAN FirstChance)ExceptionRecord 是指向异常记录的指针,ContextRecord 是指向线程上下文(CONTEXT) 结构的指针,ExceptionFrame 始终为x86 平台NULL,TrapFrame 是堆栈帧的基地址,FirstChance 指示这是异常的第一轮(TRUE)还是第二轮(FALSE)处理机会。
内核中的代码可以通过RtlRaiseException调用NtRaiseException和KiRaiseException(相当于NTDLL.DLL中的版本)。也就是说,无论是从用户态调用RaiseException API,还是从内核态调用对应的函数,最终都会走向KiRaiseException。
对于Visual C++程序抛出的异常,例如MFC中从CException派生的各个异常类对应的异常, throw关键字直接对应于CxxThrowException函数。 CxxThrowException 会调用RaiseException,并将ExceptionCode 参数固定为0xe06d7363(对应的ASCII 码为.msc)。接下来的过程和上面直接调用RaiseException的情况一样。由于C++异常的实现与编译器相关,因此本书仅讨论Visual C++编译器的使用。
.NET程序抛出的异常(CLR异常)也是通过RaiseException API生成的,其异常代码固定为0xe0434f4d(对应的ASCII码为.COM)。
综上所述,无论是CPU异常还是软件异常,虽然原因不同,但最终都会调用内核中的KiDispatchException来分发异常。也就是说,Windows系统采用统一的方式来分配CPU异常和软件异常。
11.3 异常分发过程
根据前两节的介绍,当异常发生时,CPU会通过IDT找到异常处理函数,即内核中的KiTrapXX系列函数,然后执行。然而,KiTrapXX 函数通常只是简单地表示和描述异常。为了支持调试和软件定义的异常处理功能,系统需要将异常分发给调试器或应用程序处理功能。对于软件例外,Windows系统基于和CPU
异常统一的方式来分发和处理的,本节将介绍分发异常的核心函数KiDispatchException和它的工作过程。
用户评论
这篇文章写的真不错!对我来说很清晰地解释了 Windows 中的中断和异常机制工作原理。之前总是觉得这些都是黑盒,没有明白里面的细节是什么样的。现在我终于理解了为什么程序会突然暂停或者崩溃的原因啦!
有9位网友表示赞同!
我觉得文章里分析的有条理,尤其是中断和异常之间的区别讲解得非常到位。我一直有个疑问,就是当一个进程发生异常的时候,会不会影响到其他的进程呢? 文章中提到了硬件检测机制和内核态/用户态之间的切换,让我对整个系统的运作有了更深的理解。
有7位网友表示赞同!
学习操作系统确实挺费时间的,这篇文章讲的操作中断和异常机制,我之前也遇到过类似的难题。其实很多时候都是因为代码逻辑错误导致程序出现异常情况。希望后续能多一些关于调试和解决程序异常的方法介绍!
有15位网友表示赞同!
虽然标题明确了是Windows系统的角度讨论,但我觉得如果能够再多说一点其他主流操作系统的机制对比的话,会更有意思!
有9位网友表示赞同!
看了这篇文章,我更觉得操作系统的设计真是太复杂了!从中断处理到异常机制,每一环都在保证系统安全和稳定运行。对软件工程师来说,理解这些基础原理是非常重要的!
有18位网友表示赞同!
说实话,我感觉文章分析的有些过于详细了,对于初学者来说可能会比较难以理解。我觉得如果能用一些通俗易懂的例子来辅助解释,会更有助于读者入门学习。
有12位网友表示赞同!
这篇文章的写作风格有点像教材,我很怀念以前那些有趣的博客帖子,让人一看就想去试试!希望未来能看到作者写一些更活泼、更引人入胜的文章!
有20位网友表示赞同!
文章里提到的中断和异常类型分类方法很有逻辑,但我觉得对于每个类型的具体处理方式,可以再多详细解释一下,比如在遇到特定类型的异常的时候,操作系统会采取哪些措施来解决问题?
有17位网友表示赞同!
Windows的操作系统的机制确实很强大,这篇博文很好的阐述了中断和异常机制的运作原理。学习后感觉自己对Windows系统有了更深层的理解!
有18位网友表示赞同!
这篇文章的内容很有深度,对我来说非常有帮助!我想要学习更多关于操作系统内核的设计,期待看到作者分享更多关于这个方面的知识!
有13位网友表示赞同!
我对程序异常机制一直很感兴趣,这篇博文让我对windows系统的处理方式有了更深入的了解。希望以后能有机会学习其他的操作系统是如何处理中断和异常的!
有19位网友表示赞同!
我感觉文章的结构有点混乱,逻辑衔接的地方可以再完善一下。读起来感觉有点跳跃,不利于理解整体内容。
有5位网友表示赞同!
这篇文章写得比较学术,对于非专业人士来说可能很难完全理解。希望能用更通俗易懂的语言解释一些技术细节!
有12位网友表示赞同!
我个人不太认同文章中关于中断和异常机制的分类方法,我觉得可以从程序员的角度出发,对不同的异常类型进行更为细致的分析!
有15位网友表示赞同!
这篇文章让我意识到了操作系统在日常生活中默默地保护着我们。感谢作者分享这篇有益的知识!
有16位网友表示赞同!
我之前一直以为中断和异常都是一样的概念,读完这篇文章后才知道它们其实是有区别的。这种深入的分析确实很有价值!
有15位网友表示赞同!
学习操作系统真是太不容易了!但是看到像这样清晰易懂的文章,让我对以后继续学习充满了信心!作者辛苦啦!
有18位网友表示赞同!