从CPU到内核/到用户态全景分析异常分发机制——硬件基础及Hook

 

0、引言

【直接看最后一张图——硬件HOOK,你会感兴趣的】讲解中断或者异常的,大部分书籍仅仅简单的提一句,并没有过多的往深了去讲解。主要原因是想要彻底弄清这个问题,必须要到硬件层面才行,否则都是浅显的失败尝试。很多教cpp的老师,讲解下try catch throw就基本完事了,连try except这些都没讲过,更别说什么全局展开,异常分发了。要想弄明白这底层的原理,不看看代码怎么能说懂了呢?这个系列从异常/中断的起源讲起,一直讲到你的程序是如何接管异常为止,每一个细节都不会跳过。

整个系列涉及到的知识:

1、CPU异常与中断概念与区别;
2、Intel架构中的IDT表与ISR;
3、Windows内核中内核异常分发;
4、Windows内核中用户态异常分发;
5、在调试器里查看PIC中断和APIC中断;
6、硬件HOOK的另类实现

 

1、基础知识

往下看之前,请各位先问下自己,以下几个问题,如果都了然于胸的话,这小节可以直接跳过;
问题1:异常、中断的本质区别是什么?
问题2:是谁先发现的异常、中断?
问题3:异常、中断是依靠什么方式来管理的?
问题4:CPU是如何将异常转交给OS的?

看到这里,说明你没能完全回答上边的内容罗,没关系,接下来一块来看看。首先解释异常是什么。异常就是CPU跑着的过程中,遇到了一些很“郁闷”无解的事情,诸如:无法识别的指令,缺页,硬件错误,除0等等。CPU也不傻,遇到这些问题的时候,它当然不会藏着掖着,他也不背锅,直接甩出去了。再来解释中断是什么。中断就是外设与CPU“打电话”的工具,就是CPU跑的好好的,当然也可以是正睡着觉,突然“有人”来敲门了,比如网卡接收到数据了,串口接收到数据了,RTC定时时间到了,键盘被按下了,鼠标被按下了。。。当这些外部设备需要告知CPU时,他就通过“中断”打个电话给CPU——你这老小子别玩了,来客了,快点接单,不然我就往死了等你。下边给大家画一张图,直观的看一下远古时代,通过两片8259芯片级联的方式来接收外部中断的方式,8259芯片就是俗称的PIC——中断控制器;

简单解释下上图,每一块8259芯片都可以单独使用,IR0-IR7分别时8个中断源,但你知道的,8个够个屁啊,那么多外设,聪明的前辈们当然也知道,一种比较常规的解决方案就是“级联”两块8259芯片,这样就可以搞定15个外设了,对是15个而不是8+8个。那么级联的这两块8259一块称之为Master另一块称之为Slave。还有个问题,鼠标和键盘假设同时触发了一个中断,那8259该如何处理?CPU又该如何处理?嗯,这是个好问题,8259的处理方式很简单,给每个外设设置优先级,按照优先级依次报告给CPU。CPU呢,也是差不多,如果当前没有处理中断,则8259报过来了我就处理,如果当前CPU正在处理中断,那就比较下谁的优先级高就处理谁的。注意,这句话隐含了另一层含义,即CPU可以嵌套处理,当前的高优先级中断处理完毕,CPU会/能够返回到上一个中断程序继续处理未完成的中断。好了,再看下异常,CPU支持哪些异常呢?鬼知道,只有CPU生产厂商才知道,这些都是预定义的,那就来看下Intel的CPU预定义的那些个异常吧。Intel白皮书卷三中相关内容,如下:

挑几个常规的说一下,比如第一个除0异常,显然就是CPU发现被除数是0了,主动报告了这么一个异常,那么CPU是如何发现的呢?来看看Intel给的检测这个异常的代码:

简单明了,做了检测的,很可能有人会说,这个只是伪代码。其实,你用VHDL或者Verilog开发过CPU,你会发现,你的代码中也是这样写的。大家可以下一些开源的CPU代码验证下。再来看一下三号异常Breakpoint,这个在调试中是用的最多的——喔嚯,居然调试是借助异常来实现的。简单解释下CPU报告这个异常的过程,当CPU内部的译码器译码完,发现当前的指令是“0xCC”,则会通知CPU报告一个断点异常。

好了,总结下几个关键的点:
1、中断是外设需要通信时,通过PIC报告给CPU的;
2、异常是CPU自己跑着跑着发现不对劲,自己报告的;
3、中断是异步的,异常是同步的;

 

2、PIC与CPU的通信

第一节看完后,应该有千千万万个为什么,比如我能猜到一两个;比如没有PIC的话,CPU就不能与外设通信了吗?PIC是怎么把中断号告诉CPU的? 一个一个来解答,第一个,没有PIC的话,CPU当然可以与外设通信,而且解决方案还不止一种,比如:CPU通过轮循的方式一个一个的询问外设,你有事没,有的话赶紧说,不然下一个。这种方式的缺点显而易见,效率太低——如果外设太多还会导致响应不及时呢。另一种解决方案就是,为每个外设分配一个IO引脚,这样就把PIC给彻底抛弃了,比如51单片机就是这么干的,如下:

但这也有个很明显的缺陷,外设越多占用的IO脚就越多,显然也划不来。那显然比较下来,还是搞一个PIC要划得来,既然PIC话的来,那如果是15个外设的话,PIC又是如何把中断号告诉CPU的呢?且看我下图所示:

这个过程细节虽然不要扣,但有个映像还是必要的。这些数据可以通过示波器来抓波形看,有条件的可以试试。下边给出一个典型的PIC与CPU连接的系统图;

上图中IDT先不用管,后边会讲解到。当然了这会Windbg怎么能缺席呢,来看一下:

0: kd> !pic
----- IRQ Number ----- 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
Physically in service: . . . . . . . . . . . . . . . .
Physically masked: Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y
Physically requested: Y . . . . . . . Y Y . . Y . . .
Level Triggered: . . . . . . . Y . Y Y Y . . . .

说明下,Y表示的是yes,即1。

Physically in service:表示当前CPU正在处理的外设;由上图可知,当前没有外设中断被处理;
Physically masked:表示屏蔽掉哪些外设,这是全屏蔽,奇怪不?这个谜题在下一节解开;
Physically requested:表明当前有哪些外设正在请求中断处理;

再在Windbg中读一下外设数据吧,前提是你要知道一些外设的IO端口,整理了下,如图所示:

Windbg读些外设IO的操作如下图所示:

 

3、时代在发展,PIC的替代者——APIC

时代的洪流滚滚向前,15个外设显然不够用了,后来Intel憋了个大招,将PIC升级成了APIC,即Advanced PIC。来看看这玩意咋玩的。就给一张图吧,详细的文档需要去看Intel的白皮书,主要是几个寄存器是啥意思,怎么配置等。其他的跟8259的没本质区别;

从IRQ0到IRQ23的中断分配如下表所示:

有一些需要说明的问题,为了配合图片突出显示一些内容,下边的内容我放在了Excel中整理了,截图如下:

 

4、关于硬件HOOK的一些安全相关思考

(完)