中断通常是由CPU的外设所触发,供外部设备通知cpu进行处理。
这样做的相对一面就是轮询。
中断: 通常来自于外部设备,是中断源发起的,cpu是被动的。
异常: 来自与cpu自身,是cpu主动产生的。
作为一个特例,一般把int n产生的中断,称为异常。以为其行为更加符合异常,即由cpu自身产生。
异常分为3类: 错误,陷阱,中止。
错误: 导致错误的异常,一般都可以被纠正,然后继续无损的恢复执行。比如缺页异常。
需要注意的是: 当此类异常发生的时候,堆栈中保存的cs:esp 还只向发生错误的那条指令。
即当cpu转而执行异常处理程序返回后,还是要继续执行导致异常的指令。
陷阱类异常: 和错误有点相似,也是可以恢复的;不过差别在于,其发生时候的cs:eip只向下一条待执行指令。 比如int 3导致的。当int3的isr执行后,显然不是继续eip指向int3。
中止: 严重错误,无法恢复,且cpu无法保证其报告的导致异常的eip是精确的。
另外从概念上来说,中断是cpu提供的一种正道,能力;而异常就肯定是一种“不正道”,错误类型的方式。
但是从处理上来说,它们的处理过程都查询IDT表来处理。
由于异常既然是一类错误,因此cpu在产生某些异常时候,回向stack中压入一个32bit的错误代码,格式如下:
bit 0 : EXT , 1表示外部异常;
bit 1: IDT : 1 表示错误码的段选择子索引指向IDT表。
bit 2: TI : IDT=0 有效; 1表示指向LDT, = 0 指向GDT。
bit 3 - bit 15: 13bits , 段索引符.
下面看一个简单的异常处理例子:
当cpu被中断后,执行的过程叫做ISR,中断服务过程。
这个时候,可以通过windbg的local or kernel模式,展示一下!idt -a ,设置好,符号就可以看到看到中断描述符表中的ISR 列表。
结合前面讲到的GDT,我们讲了GDT中存储了段描述符,实际上中断描述符表的入口也在那儿。
这儿回忆一下一个逻辑地址和到线性地址是GDTR指出GDT的地址,段寄存器指出基址,然后加上逻辑地址;
这儿中断入口地址如何确定的呢?
首先IDT表的入口地址存储再IDTR 中,然后 INT x , 中断号相当于索引,这样就计算出了ISR地址,
可以在kernel 模式下,通过!idt -a ,然后观察第三号中断ISR;
这儿也可以考虑,结合vpc2007 的kernel windbg 和vc6进行一个demo,展示3号中断的处理过程;以后kernel模式结束后的用户态模式切换;
我们来看一个例子:
首先启动vpc 中的win xp,且启动其中的windbg,设置断点,且看到__asm int 3 执行前的情况如下:
下一步在真实xp中的kernel debug中设置硬件断点,且记录当时内核stack的一些信息。
下面我们让内核调试器继续运行,在用户态调试器中让testing3继续运行。
很容易发现堆栈从顶往下是:
testint3 的eip eip = 401021
testint3 的代码段 cs = 1b
testint3 中断前的efl,即eflags的低16位=216
testint3 的堆栈指针 esp = 12ff70
testint3 的堆栈段 ss = 23
从这个内核堆栈,即使不仔细说,其实大家也能够知道大致cpu帮我们干啥了。
isr 内部干了些啥,这儿就暂时不分析了,有机会以后分析。
这儿也可以留几个问题,即保护模式下,用户态代码和内核态代码在这种情况下是如何切换的?
下面让kd调试器继续运行,我们来观察用户态windbg.
这个时候发现
cs eip
ss esp
efl 是不是和刚才哪个内核堆栈很像, 除了esp有4个自己的差异。 这个差异,这儿不做分析,就当作内核返回时候的一个esp调整。
但是总是能够从这个调试观察过程,看到中断时候用户态,内核态的一些细节,推敲出一些行为。
下面再看看调试器设置的断点是什么样子的?
其实发现,debugger干的也就是这么一回事。
下面讲一下硬件断点。
一般来说,我们通过vs系列,或者一般应用软件的gui界面设置的断点都属于软件断点,或者就像前一个例子那样,实际上就是+了一个int 3指令;
那么什么是硬件断点呢?
硬件断点是cpu内置支持的,这种断点的设置不需要修改应用程序代码,注意这个很爽喔。
不过硬件断点只能设置4个。
刚才我据例子的时候用到的ba指令就是设置硬件断点。 我们来看一下:
dr0-dr3 3个寄存器设置4个hw 断点地址;其中dr7是控制寄存器,
dr7的bit0=1表示dr0有效。
从资料知道,windbg 的ba命令,以及vs2005的数据断点都采用的是hw断点,但是vc6除外。
vc6是通过单步比较方式,查表确定是否match。
这儿提一个问题,大家考虑下调试器的源码单步执行,调试器大概是如何做的?
1. TF
2. run 2 address
下来讲讲IDT。
记得之前讲到保护模式时侯说到GDT表中的段描述符有一类称为系统描述符,而系统描述符则包含终端和门描述符,而这儿讲到IDT时侯又说有中断门之类的,如下。
首先需要说明,这是两个不同的东西。
1. IDT表中描述的是门的内容,格式。
2. GDT中的系统描述符是IDT表中门描述符中段选择子指向的。
3. 最终定位门的内容将是
a) 先从IDT表中定位,通过段选择字指向gdt
B) 从GDT定位最终在内存中
下面我们举个例子: 下面是TSS的格式
任
务
状
态
段
基
本
部
分
的
格
式
BIT31—BIT16
BIT15—BIT1
BIT0
Offset
0000000000000000
链接字段
0
ESP0
4
0000000000000000
SS0
8
ESP1
0CH
0000000000000000
SS1
10H
ESP2
14H
0000000000000000
SS2
18H
CR3
1CH
EIP
20H
EFLAGS
24H
EAX
28H
ECX
2CH
EDX
30H
EBX
34H
ESP
38H
EBP
3CH
ESI
40H
EDI
44H
0000000000000000
ES
48H
0000000000000000
CS
4CH
0000000000000000
SS
50H
0000000000000000
DS
54H
0000000000000000
FS
58H
0000000000000000
GS
5CH
0000000000000000
LDTR
60H
I/O许可位图偏移
000000000000000
T
64H
1. 通过!pcr or r idtr 首先拿到IDT表的地址
2. !idt -a
3. 如果符号设置无误,一般能够看到8号为TSS任务段。
实验: 通过TSS段来观察TSS的实际内容 , 上面的表格实际上就是TSS的内容格式。
1. db addof(IDT)+ 8*INDEXof(TSSindex) L8
通过IDT表的门格式分析,很快发现50就是段选择符.
2. dg 50
这儿我们就发现它位于内核区 80552200 68字节。
3. dd 80552200
从格式知道,offset=32的是eip
4. ln addr
果然发现 nt!KiTrap08 ,呵呵,第8号处理函数.
先述说os的基本的几个重要函数吧。
RaiseException --> NtRaiseException --> nt!KiRaiseException
其中前者一般是由用户程序触发,比如throw,中间的位于ntdll内,通过前面的介绍,它就是发起fastcall调用进入内核的,最后的是内核中的处理函数。
在windbg 中: (exexception_test)
1. 设置断点在RaiseException, then g
2. t ,enter
3. pc 2 find next call
4. 最后跟踪到 sysenter 前
5. 启动kd,在kd中设置 KiRaiseException的断点,g
6. windbg , g
7. kd 中捕获到断点
8. !process 0 0 exexception_test 。。。 设置号合适的上下文,对其符号
9. 这个时侯你就会发现一个完整的exception抛出的处理过程。
而kiraiseexception 的内在过程,纯粹是汇编分析,这儿不做分析,而是引用软件调试的一张图。
这儿我们以分析用户态的异常为主,所以有一个问题:即从kiraiseexception如何回到用户态,
如何回到用户设定的handler?
由于在测试的时侯,同样遇到了sysenter指令的问题,即vpc2007在一些硬件指令上的问题。导致了vpc直接crash。
但是大致我们可以从软件调试中知道,最终内核态中的KiDispatchException 设置好KiUserExceptiondispatcher 的用户态地址,然后返回用户态就从此执行。
而此函数将执行 RtlDispatchException . 最后这个过程,我们可以从用户态调试器来观察.
实际上设置了 7c92e47c kiuserexceptiondispatcher 断点后,当break下来后,也会发现stack实际上是空的。
继续跟踪下去,也会发现rtldispatchexception 会被调用。
这个调用,大致能够看出是在检查是否存在合理的handler处理。
在dt _EXCEPTION_REGISTRATION_RECORD -l Next eax [fs[0]]
歪歪一下: _EXCEPTION_REGISTRATION_RECORD 是从w2k的流传src得到的呵呵!
后,设置上所有的handler的断点,很快逐步根据,会发现如下图:
即用户态的filter在万水千山之后终于倍hit。
当再次进入printf后,stack unwind后,就没啥可以看了。呵呵,stack unwind,后面再说了。
{
stack unwind 具体原因这儿不说,不过我可以据一个例子大家来看看.
// stack_unwind.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include
void TestFunc()
{
__try
{
int i = 0;
i = i / i;
}
__finally
{
printf("__finally \n");
}
}
int FilterFunction()
{
printf("FilterFunction \n");
return EXCEPTION_EXECUTE_HANDLER;
}
int _tmain(int argc, _TCHAR* argv[])
{
__try
{
TestFunc();
}
__except(FilterFunction())
{
printf("handler \n");
}
return 0;
}
FilterFunction
__finally
handler
什么是stack unwind ,这下应该有个明确的了解了吧。
__finally 就是在stack unwind的时侯倍执行的终结器
}
VEH 这个概念是win xp开始引入的,由于不具有通用型,因此不做详细demo。
只要知道rtldispatchexception 在寻找SEH handler前会给VEH handler一个机会处理,
且VEH是全局范畴的,不基于stakc的。
也就是说如果你想写一个通用的handler,这到不错,呵呵。
软件调试11.5做过比较
1. SEH 信息注册在stack frame中
2. VEH 优先于SEH 被call
3. VEH 存储在mem中,全局的,进程中通用
4. SEH 通过编译器处理生成数据结构。VEH通过主动API调用建立。
下面讲讲unhandledexception