This page looks best with JavaScript enabled

Windows保护模式学习笔记(6)—— 中断与异常

 ·  ☕ 6 min read · 👀... views

Windows保护模式学习笔记(2)—— 调用门&中断门&陷阱门 中介绍了一些中断和异常的概念,这里重新做一个系统的阐释。

无论是由硬件设备触发的中断请求还是由CPU产生的异常,处理程序都在IDT表中!

0x01 中断

中断通常是由CPU外部的输入输出设备 (硬件) 所触发的,由外部设备通知CPU“有事情需要处理”,因此又叫中断请求(Interrupt Request)。中断请求的目的是希望CPU暂时停止执行当前正在执行的程序,转去执行中断请求所对应的中断处理例程(中断处理程序在哪有IDT表决定),其本质是改变CPU的执行路线。假设一下,如果没有这种机制,如果我们编程的时候写了一个死循环,程序就不会响应我们外部的终止信号(比如Ctrl+C),从而一直的执行下去。在80x86架构中有两条中断请求线,分别是:可屏蔽中断线,称为INTR(INterrupT Require)非屏蔽中断线,称为NMI(NonMaskable Interrupt)

可屏蔽中断

描述:

  1. 在硬件级,可屏蔽中断是由一块专门的芯片来管理的,通常称为中断控制器
  2. 它负责分配中断资源和管理各个中断源发出的中断请求
  3. 为了便于标识各个中断请求,中断管理器通常用**IRQ(Interrupt ReQuest)**后面加上数字来表示不同的中断
  4. 比如:在Windows中,时钟中断的IRQ编号为0,也就是IRQ0

时钟中断

描述:

  1. 大多数操作系统时钟中断在10-100MS之间,Windows系列为10-20MS
  2. Windows时钟中断每隔10~20MS会向CPU发送一个请求,当CPU收到请求时,操作系统就会接管CPU,指定CPU去执行一段代码,操作系统在这段代码里便有机会进行线程的切换。
  3. 这样设计,即便一个程序进入死循环,操作系统依然有机会进行线程切换
  4. 当然,操作系统主要并不是通过时钟中断来进行线程切换,而只是有机会进行线程切换,这里只是举个例子。

可屏蔽中断的处理

前面说过,时钟中断的IRQ编号为0,所在位置为IDT[0x30],而IRQ1~IRQ15分别对应IDT[0x31]~IDT[0x35]

注意:

  1. 如果自己的程序执行时不希望CPU去处理这些中断,可以:
    用CLI指令清空EFLAG寄存器中的IF位
    用STI指令设置EFLAG寄存器中的IF位 \
  2. 硬件中断与IDT表中的对应关系并非固定不变的,参见:APIC(高级可编程中断控制器)

不可屏蔽中断的处理

  1. 非可屏蔽中断处理程序位于IDT表中的2号位置
  2. 当非可屏蔽中断产生时,CPU在执行完当前指令后会马上进入中断处理程序
  3. 非可屏蔽中断不受EFLAG寄存器中IF位的影响,一旦发生,CPU必须处理

0x02 异常

异常通常是CPU在执行指令时检测到的某些错误,比如除0、访问无效页面、权限不足等。其与异常的区别在于:

  1. 中断来自于外部设备,是中断源(比如键盘)发起的,CPU是被动的
  2. 异常来自于CPU本身,是CPU主动产生的
  3. INT N虽然被称为“软件中断”,但其本质是异常
  4. 因为INT N本质上是异常,所以EFLAG的IF位对INT N无效。

常见的异常处理程序:

页错误:当我们访问一个线性地址,而这个线性地址指向的物理页是无效的,便会触发CPU异常,该异常位于E号门(IDT[0xE])
段错误:一旦段的运算发生异常时(如权限检查),便会走D号门(IDT[0xD])
除0错误:当除数为0时,会触发异常,这时走0号门(IDT[0x0])
双重错误:假设执行一个异常(如页错误)时又产生了一个错误,那么便会触发双重错误,这时走8号门(IDT[0x8])

缺页异常(IDT[0xE])

较为常见引起缺页异常的情况有两种

  1. 当PDE/PTE的P=0(有效位为0)时会发生缺页异常
  2. 当PDE/PTE的属性为只读但程序试图写入时会发生缺页异常

一旦发生缺页异常,CPU会执行IDT表中的0xE号中断处理程序(页错误),程序由操作系统来接管。

例一:P=0异常

什么时候会使一个物理页的P位(有效位)为0呢?前面我们说过若一个物理页是有效的,那么PDE/PTE的P位一定为1。但是当其他进程的物理页发生紧缺(不够用)时,但当前进程的当前线性地址是有效的(指向了这个物理页),那么操作系统就会把这个物理页的内容保存到文件里,再将这个物理页挂到别人的PDE/PTE中供其使用,最后将当前进程指向这个物理页的线性地址的PDE/PTE的P位改为0,其意为这张物理页对于当前进程无效,其被用在了别的进程上了。

那如果这时我们访问这个线性地址时,操作系统发现其P位为0,便会进入0xE号中断处理程序(IDT[0xE])。进入IDT[0xE]后,若操作系统发现其P=0、转移位=0、原型位=0,而其他位都是有值的时候,就说明当前物理页被存储到了页面文件里,页面文件的位置(编号)保存在PFN中。操作系统根据编号找到页面文件后,会把里面的内容从文件中再次读到物理页上,并将P位改回1。

这整个过程对于用户来说是完全透明的,用户并不知道发生了一个异常,只知道程序能够对地址进行正确的读写,但其实这个过程中可能有大量异常在发生。操作系统通过这种异常的方式节省大量物理页。当程序在执行时,这种缺页异常时时刻刻在发生。

例二:权限异常

当一个线性地址的PDE/PTE属性为只读,但试图往里写时,也同样会触发页错误异常。CPU检测到了这个异常,便进入0xE号中断处理程序(IDT[0xE]),由操作系统来接管。如果操作系统检测出用户的操作确实是不合理的,便会返回一个错误(0xC0000005内存访问失败

异常、IDT与SEH

相信做过开发或者在CTF赛事中做过re题的话,一定会知道在实际编程中有一种叫做**SEH(Structured Exception Handling,结构化异常处理)**的东西,这个东西的存在也是帮助程序主动处理异常的。那么问题来了,SEH和IDT之间是一个什么关系呢?当异常发生时,它们的调用顺序又是怎么样的呢?

这里特别感谢Bayerischen师傅的解惑。首先,它们之间是没有直接的关系的,或者说,它们两个东西就不是一个“次元”的。同样的,给一个程序设置上SEH,不会对其IDT产生任何改变!

当异常发生时,CPU一定是会先进IDT,通过IDT进入系统内核态进行一顿操作,操作完之后才会回来到用户态找异常处理函数,也就是SEH。也就是说,并不是设置了SEH就相当于修改了IDT,发生异常直接通过IDT跳到SEH的异常处理函数的!他们没有直接的关系!SEH只会修改fs,等CPU在内核态中操作完以后会回用户态检查fs有没有设置SEH,若有再执行SEH上的异常处理函数。

0x03 学习资料

  1. lzyddf师傅的博客

Share on

Qfrost
WRITTEN BY
Qfrost
CTFer, Anti-Cheater, LLVM Committer