首页
社区
课程
招聘
[原创]科锐第三阶段项目之一,调试器的设计(1、15、31、38、40、47、50楼更新)(源码.文档.使用说明,全都在一楼更新了)
发表于: 2010-4-27 23:58 61090

[原创]科锐第三阶段项目之一,调试器的设计(1、15、31、38、40、47、50楼更新)(源码.文档.使用说明,全都在一楼更新了)

2010-4-27 23:58
61090
收藏
免费 9
支持
分享
最新回复 (76)
雪    币: 167
活跃值: (1574)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
26
O 原来昨天是在说这个哦

Up Up ~~
2010-4-29 19:02
0
雪    币: 421
活跃值: (83)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
27
科锐学员太猛,太牛了。精华遍地。
2010-4-29 19:41
0
雪    币: 15
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
28
啊呀。 传说中的黄牛现身了
2010-4-29 20:45
0
雪    币: 200
活跃值: (26)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
29
顶哦~皇兄。 明天上课请我吃早餐哦
2010-4-29 21:12
0
雪    币: 3
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
30
神牛阿,
2010-4-29 23:25
0
雪    币: 378
活跃值: (702)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
31
关于内存断点的设计与实现
第二章 内存断点        9
2.1 问题的提出与分析        9
2.1.1 问题提出        9
2.2问题的解决        9
2.2.1 内断断点与内存分页的关系        9

同时附件为调试器的可执行程序(修复了一些小bug)
第一章节与第二章节的内容
以及pe解析部分的动态邻接表的源代码(我的代码写得很恶心,看的话做好心理准备,哈哈)

多谢张文君(金山毒霸)师姐指出了程序的一些小bug

                                                          第二章 内存断点
2.1 问题的提出与分析
2.1.1 问题提出
因为内存断点是调试器的一个难点,所以我们这里放在最前面讲,这样也可以避免在后面的设计中一些组合问题的产生。
一个内存分页可能会存在多个断点,一个断点可能会跨多个内存分页。某个内存分页可能不具有用户要设的断点属性。某内存分页具有读与写的权限,但是用户只下了写的断点,如果将读的权限也一起拿掉,程序在读那块内存分页也要断下来,这样将大大的降低程序的效率,如何优化,以及断点信息与内存信息的存储。

2.2问题的解决
2.2.1 内断断点与内存分页的关系
一个内存分页可能会存在多个断点,一个断点可能会跨多个内存分页。这个不由的让我们想起第一阶段的学生管理系统(一个学生可以选任意门课程,一门课程可能有任意个学生选)。其实二者的关系是一样的,因为是多对多的关系,所以我们要构造出第三张表来管理他们的关系。
为了检测用户下断点有效性等问题,在下断点的时候取得那个内页分页信息,判断地址是否有效,然后再判断是否具有要断下来的属性。对一个没有读属性的内存分页下读断点是无效的。
为了提高效率,中间表用了二个,一个是以内存页序号为主键,一个是以断点序号为主键,使用动态邻接表(长度自动增长)存储,这样可以提高程序的处理速度。当调试器收到异常信息,首先取得产生异常的地址,以及错误编码,再通过异常地址取得内存页的序号,再通过内存页的序号去邻接表里查他有多少个断点,如果当前有内存断点的话,恢复内存页原属性,然后再判断当前位置是否处于用户设的断点地址内,是的话,处理用户的请求,最后设单步,在单步中再将内存页的属性设成新的属性。

添加内存断点流程图                    处理异常流程图


内存断点本来就是一件很耗资源的,为了最大的提升效率, 当处理某内存分页具有读写的权限,而用户只下读或写的权限的这类情况,我们可以通过对原属性的分析,然后再减去用户新的属性,再根据结果生成新的属性,因为移除某个属性,并不能单单的通过去除某个位而完成。
相关记录项的结构:


内存页与断点关系表(邻接表):
上传的附件:
2010-4-30 23:44
0
雪    币: 364
活跃值: (152)
能力值: ( LV12,RANK:450 )
在线值:
发帖
回帖
粉丝
32
呃,你居然认识张文君...
2010-5-1 00:05
0
雪    币: 378
活跃值: (702)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
33
哈哈,今天就是她告诉我,你也是金山的!到时回家叫张文君介绍你给我认识,没事可以找你玩,哈哈,我家住新香洲那!很近的!
2010-5-1 00:09
0
雪    币: 364
活跃值: (152)
能力值: ( LV12,RANK:450 )
在线值:
发帖
回帖
粉丝
34


她怎么知道我ID的……欢迎来玩,呵呵,这边有很多牛人介绍给你认识
2010-5-1 00:25
0
雪    币: 2362
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
35
张文君是不是打篮球中锋位置的那个?
2010-5-1 00:52
0
雪    币: 378
活跃值: (702)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
36
或许是你的调试器出卖了你吧!
2010-5-1 01:25
0
雪    币: 378
活跃值: (702)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
37
张文君是PLMM来的!不打蓝球的!
2010-5-1 01:31
0
雪    币: 378
活跃值: (702)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
38
PDF加了目录,可以快速的定位.
附件pdf文档,以及用到的部分功能函数

第三章 int3断点与硬件断点
3.1 问题的提出与解决
3.1.1 问题提出
如何实现软硬件断点?如何判断用户断点的正确性?如何提高效率?
3.2 问题的解决

3.2.1 Int3断点的实现
8086/8088提供断点指令Int3和单步标志TF,调试工具利用它们可以设置断点和实现单步。从80386开始,在片上集成了调试寄存器。利用这些调试寄存器不仅可以设置代码执行断点,而且还可以设置数据访问断点,不仅可以把断点设置在RAM中,也可以把断点设置在ROM中。
Int3是Intel系列CPU专门引入的用于表示中断的指令,对于Int3断点,常见的做法是在用户要下断点的地方,将指令替换成CC,当调试程序运行到CC处,调试器将会收到EXCEPTION_BREAKPOINT消息,然后在这里将原CC处的代码还原及EIP减一再处理用户的请求。用Int3的好处很明显,可以设任意多个断点,缺点是改变了程序的原指令,容易被软件检测到,而且这里可能会遇到一个问题,当用户在某个内存分页设了一个不可读,不可写的断点,这时调试器是无法将CC写进去的,也无法将原来的指令读出来!所以在设之前,我们先将目标内存页的属性设为可读可写的,设完之后再将内存页置为新的属性(移除了用户断点权限的新属性),这个开销是非常大的。
本程序中,对于用户断点的正确性检测只做了如下判断,首先是判断用户的断点是否是处于有效的内存分页内,然后再判断是否重复,同一个内存地址,只能设一个断点,而对于Int3断点和硬件执行断点并没有判断是否处于指令起始处。
当初的设想是被调试程序跑起来的时候,将被调试程序的指令全部解析一下,然后取得每条指令的正确起始位置,添加到树中,然后用户在下Int3断点的时候,再判断一下下的断点是否处于指令的首地址处!后来请教了一下钱老师以及查看windbg等同类软件,发现也没有做这类的检测,同时分析觉得此弊大于利,所以放弃了这种做法,程序中并没有检测。

3.2.2 硬件断点的实现
    80386和80486都支持6个调试寄存器,如图:

    他们分别是DR0、DR1、DR2、DR3,调试状态寄存器DR6和调试控制寄存器DR7。这些断点寄存器都是32位寄存器。
Dr0~3用于设置硬件断点的线性地址,也就是断点位置,产生的异常是STATUS_SINGLE_STEP 。这些寄存器长32位,与32位线性地址长度相符,处理器硬件把执行指令所涉及的线性地址和断点地址寄存内的线性地址进行比较,判断执行指令是否触及断点。与Int3不同的是,硬件断点不仅可以设置在RAM中,也可以设置在ROM中,且速度比Int3更快。
Dr7是一些控制位,用于控制断点的方式。其中有四个GiLi,分别对应四个断点,Gi、Li控制DRi所指示的断点i在断点条件满足是,是否引起异常。
Gi和Li分别称为全局断点允许位和局部断点允许位。在任务切换时,处理器清各Li位,所以Li位只支持一个任务范围内的断点,任务切换并不影响Gi位,所以Gi支持系统内各任务的断点。
R/Wi分别对应四个DRi,R/Wi字段指示DRi断点信息的类型。每一个R/W占两位,所表示的类型为:
RWE字段取值    断点类型    RWE字段取值    断点类型
0 0    只执行    1 0    未定义
0 1    只写入    1 1    只读或写数据
LENi字段指示DRi断点信息的长度,每一个LENi占两位。所表示的长度如下:
LEN字段取值    断点长度(范围)    断点地址寄存器指示的断点地址
0 0    1字节    全部32位指示一个单字节的断点
0 1    2字节    最低1位被忽略,确定字对齐开始的2字节断点
1 0    未定义(不能取该值)   
1 1    4字节    最低2位被忽略,确定双字对齐地址开始的4字节断点
注意: 指令执行断点的断点长度必须为1字节,数据访问的断点长度可以是1、2、4字节。
Dr6用于显示是哪些引起断点的原因,如果是Dr0~3或单步(EFLAGS的TF)的话,则相应设置对应的位。
Bi表示由DRi所指示的断点引起的调试陷阱。
对于DR7的设置,原本程序中是使用位移来完成的,但是调试的时候非常之不方便,所以后来改用结构体了。结构体的定义如下:


3.2.3 对于int3断点的优化
    当设int3断点的时候,判断一下当前调试寄存器有没有空闲的,有的话则优先使用调试寄存器,因为int3断点涉及内存分页属性,读目标进程内存,写目标进程,而硬件断点只需设调试寄存器,这样可以大大的提高效率。
上传的附件:
2010-5-2 15:56
0
雪    币: 272
活跃值: (22)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
39
支持小奇  膜拜
2010-5-3 13:51
0
雪    币: 378
活跃值: (702)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
40
第四章 函数名的解析
4.1 问题的提出与分析
4.1.1 问题的分析
最初对于这个问题处理的想法就是分析被调试程序的导入表,后来和苏玉海讨论,发现这样不能解决问题,因为API里可能还调用了别的API,再跟进API的话,程序就不能正确解析函数名了!
据说Windows有上万个API,如何组织这些数据,如果提高查询的效率、以及最大化的合理使用空间。
4.2 问题的解决
4.2.1 函数名与函数地址的组织
     用一个链表用来存储已经加载的DLL的名字已及当前状态。为了提高查询的效率,同时记录DLL名字的CRC值,当查找时,算出要查找的DLL名的CRC值,再直接查找通过CRC值查找,当CRC值相同时,再用字符串比较是否完全相等,当加载DLL比较多的时候,或者经常要查询的话,这样可以大大的提升的效率。
        同时用红黑树记录函数名地址(函数名在被调试进程里面的地址)以及函数的绝对地址(基址加偏移后的真实地址)。
红黑树并不追求“完全平衡”——它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。红黑树能够以O(log2 n) 的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。
4.2.2 函数名的显示
在我的设计中,使用的是OD的反汇编引擎,结构体中有记录命令的类型,首先判断他是不是call、jmp这类的指令,然后再根据相应的值,找出API的地址,再判断目标地址是否是API函数的地址,是的话则从本地加载DLL链表中取得DLL名字,然后再通过函数名地址去调试进程中取得函数名(在程序中,并没有申请空间保存函数名,因为导出函数可能非常之多,不停的申请空间,然后字符串拷备,这个开销是非常之大的,所以在程序中我们只记录函数名在调试进程中的地址,同时dll名链表中有个状态值,当状态值为真时,表明那个dll当前已经加载,这样可以直接去内存拿函数名)。而对于call ebp之类的指令,而需要在运行时才解析,因为在没有执行到那条语句的时候,寄存器的值是不确定的。
效果如下:
上传的附件:
2010-5-5 00:13
0
雪    币: 422
活跃值: (115)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
41
小奇是数据结构达人啊, 我是来膜拜的
2010-5-6 15:12
0
雪    币: 148
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
42
怎么科锐的都这么牛啊??
2010-5-6 19:56
0
雪    币: 378
活跃值: (702)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
43
我是新手,还没有入门呢!有机会可以过来科锐这些参观学习呀!
2010-5-6 19:59
0
雪    币: 80
活跃值: (451)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
44
顶起大牛
2010-5-6 20:11
0
雪    币: 9
活跃值: (26)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
45
还是要向小Q膜拜啊,本来觉得有半只脚沾到门沿。
这下又要从新找门了,诶!  
2010-5-6 20:14
0
雪    币: 378
活跃值: (702)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
46
老钱教导我们不要让技术高于人品呀!所以我们都是很菜的!
2010-5-6 20:25
0
雪    币: 378
活跃值: (702)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
47
第五章 指令记录
5.1 问题的提出与分析
5.1.1 问题的分析
        当初听到这个的时候,觉得无从下手,而且也想到了很多极端的问题,比如运行的时候不停的修改原来的指令来达到往下运行的效果。然后进到API里面去了,那个记录量是非常之大的,而且当时A1Pass的测试结果是每秒只能跑7条指令.于是花了很多时间在想着效率提升这块.后来和岳磊讨论一下,他叫我学习一下OD的这个功能.
5.2 问题的解决
5.2.1 数据的组织
        因为可能记录的数据量非常之大,所以要采用一种合理的数据结构,比如用户可以按指令跑的顺序记下来,也可以按地址的高低来记录,程序在运行中,可能某个地址的指令要跑很多次,但是我们不能重复记录很多次,所以这需要一种特定的数据结构,在程序中,我使用的是带头尾指针的链表与红黑树的结合,首先是将指令加到链表尾部,然后再插到红黑树里面去,这样查找起来的话,速度非常快的(因为我们每跑一条指令,都要先判断一下树中有没有,没有的话才插到树中去),而且体积只是比单单的红黑树多一个指针域。


结合后的数据结构:这样就可以把链表与树的优点结合起来了,而补了链表的查找效率的不足。


5.2.2 指令记录实现
        在第一个解决方案中,采用的是置单步记录,但是经过测试这个效率不高,而且处理起来也很复杂,比如对于每个call之类的指令,判断是不是调用API之类的,而且经常会出现一些小问题,兼容性也不太好!
        在第二个解决方案中,采用的是内存断点的方法,这个实现起来方便快捷,也不用判断是不是调用API,而且可以指定要记录的部分,不过内存断点的开销还是挺大的。

5.2.3 效率的优化
        假定计算机访问寄存器需要花费一个单位时间片,访问cache可能要花费几个单位时间片,或许现代计算机已经可以像访问寄存器那样高速了,但是访问内存却要花费一百多的单位时间,则访问磁盘则更慢了!
        为了充分的利用计算机时间,让写入磁盘与处理同时进行,在树节点中,我们直接记录指令的十六进制的值,而不直接记录反汇编结果,这样最大的节省空间,如果对于调用API的话,在节点JmpConst中记录,如果没有的话,这个值为零。然后在写入磁盘的时间,调用反汇编引擎将Dump的内容解析出来,如果JmpConst有值的话,再转成相应的DLL名和函数名。这样就可以并发处理了,从而充分的利用了CPU的时间以及节省了内存空间。

结点内存图:


补充:
程序实现了脚本的功能,因为很简单,所以没有写在里面了!
用一个标志判断当前是从脚本输入还是从键盘输入,如果从脚本输入,则去脚本中取一行命令,如果遇到文件尾部,将标置位清零!
上传的附件:
2010-5-7 23:51
0
雪    币: 37
活跃值: (12)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
48
原来是黄奇啊!帮你顶下
2010-5-8 09:09
0
雪    币: 1349
活跃值: (485)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
49
小齐呀 顶下
2010-5-8 22:02
0
雪    币: 378
活跃值: (702)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
50
程序源代码...
程序中没有使用STL,数据结构都是自己实现的,因为当初觉得调试器的难点就在于数据结构方面,也刚好可以顺便复习一下数据结构,如果对这方面没兴趣的话那部分内容可以跳过,程序中都有注释.

如果有什么问题,或者你对这个有啥意见或者建义的话,都可以与我联系。

e-mail: evilknight.1@163.com

平台: Windows XP SP2 + VC6 sp6

源码...文档..使用说明,全都在一楼更新了!
2010-5-10 23:23
0
游客
登录 | 注册 方可回帖
返回
//