[调试器实现]调试器实现(第六章) 功能扩展
前面几章基本上已经将调试器的基本功能及其实现过程讲述的差不多了。本章作为一个结束,将补充一些前面没有提到的细节性问题,并就调试器的功能扩展做一些探讨。
单步步过的实现:
单步步过对于非CALL的指令,其实和单步步入一样,遇到CALL指令的时候我的处理方式是在CALL之后的指令首地址设置一个一次性的
INT3断点,这一点和OllyDbg略有差异。OllyDbg的做法是看当前的4个硬件调试寄存器中是否有空闲可用的,如果有就设置一个一次性的硬件执
行断点,断点地址为CALL指令后的下一条指令首地址,如果没有可用的硬件调试寄存器,才使用下一次性INT3断点的方式。因为下硬件断点比INT3断点
效率高,所以OllyDbg优先使用硬件断点。
显示系统API、DLL导出函数的实现:
大家在使用OD的时候,其API提示功能用的都很爽吧。显示DLL导出函数的方法,可以是先遍历所有DLL的导出函数,将函数名称和函数地址放
入一个链表中,反汇编过程中遇到地址值或寄存器值直接查链表匹配API;或者反过来,反汇编过程中遇到地址值或寄存器值到对应DLL的导出表中去查是否有
匹配的函数地址。由于代码比较长,且都是不停地查导出表的过程,我就不贴完整的代码了。我把我写的含有寄存器的表达式转化为对应数值的函数贴出来,让大家
帮我看看是否还有更好的处理方式,我总觉得我的处理方法比较冗长,应该还有更好的处理方法。
// 有寄存器参与的CALL指令,将寄存器表达式转化为数值 // 参数 pAddr 可能为以下情况的字符串: // eax // eax+3 // eax*4 // eax*4+ebx // eax*8+1000 // eax+ebx+3000 // ebx+eax*2+F10000 int CDoException::ExpressionToInt(char *pAddr) { char chNewBuf[30] = {0}; int nRetValue = 0; //先找有没有 * 号 BOOL isFindMultiplicationSign = FALSE; //是否找到乘号 BOOL isFindPlusSign = FALSE; //是否找到加号 int nLen = strlen(pAddr); int nMultiplicationPos; //找到的乘号位置下标 for ( nMultiplicationPos = 0; nMultiplicationPos < nLen; nMultiplicationPos++) { if (pAddr[nMultiplicationPos] == '*') { isFindMultiplicationSign = TRUE; break; } } if (isFindMultiplicationSign == TRUE) { //从乘号向前找,直到遇到加号或找到头 int nTemp = nMultiplicationPos; while (nTemp > 0 && pAddr[nTemp] != '+') { nTemp--; } //获得乘法的操作数1,必定是一个寄存器 char chOpNum1[5] = {0}; if (nTemp != 0) { memcpy(chOpNum1, &pAddr[nTemp+1], nMultiplicationPos - nTemp -1); } else { memcpy(chOpNum1, &pAddr[0], nMultiplicationPos); } int nOpNum1 = RegStringToInt(chOpNum1); //从乘号向后找 //获得乘法的操作数2,必定是比例因子2,4,8 if (pAddr[nMultiplicationPos+1] == '2') { nRetValue += nOpNum1*2; } else if(pAddr[nMultiplicationPos+1] == '4') { nRetValue += nOpNum1*4; } else if(pAddr[nMultiplicationPos+1] == '8') { nRetValue += nOpNum1*8; } else { printf("invalid scale!\r\n"); return 0; } //对 pAddr 字符串进行重组 if (nTemp != 0) { memcpy(&pAddr[nTemp], &pAddr[nMultiplicationPos+2], 20); } else { memcpy(&pAddr[0], &pAddr[nMultiplicationPos+2], 20); } nLen = strlen(pAddr); } //乘法处理完后,表达式中将只有“+”号,或没有符号,或是空字符串 if (nLen == 0) { return nRetValue; } //找加号 int nPlusPos; //从前往后找到的加号位置下标 for ( nPlusPos = 0; nPlusPos < nLen; nPlusPos++) { if (pAddr[nPlusPos] == '+') { isFindPlusSign = TRUE; break; } } if (isFindPlusSign == TRUE) { //加法之前必定是一个寄存器 char chPlusOpNum1[5] = {0}; memcpy(chPlusOpNum1, &pAddr[0], 3); int nPlusOp1 = RegStringToInt(chPlusOpNum1); //加法之后可能是一个寄存器或立即数,判断一下是否是Eax等寄存器 if (pAddr[nPlusPos+3] == 'x' || pAddr[nPlusPos+3] == 'X' || pAddr[nPlusPos+3] == 'i' || pAddr[nPlusPos+3] == 'I' || pAddr[nPlusPos+3] == 'p' || pAddr[nPlusPos+3] == 'P') { //是寄存器 char chPlusOpNum2[5] = {0}; memcpy(chPlusOpNum2, &pAddr[nPlusPos+1], 3); int nPlusOp2 = RegStringToInt(chPlusOpNum2); nRetValue += nPlusOp1 + nPlusOp2; //对 pAddr 字符串进行重组 if (nLen == 7) { return nRetValue; } else { memcpy(&pAddr[0], &pAddr[8], 20); nLen = strlen(pAddr); } } else { //是立即数,说明是最后一个操作数 int nPlusOp2 = (int)HexStringToHex(&pAddr[nPlusPos+1], FALSE); nRetValue += nPlusOp1 + nPlusOp2; return nRetValue; } } int nLast = (int)HexStringToHex(pAddr, FALSE); if (nLast == 0) { nLast = RegStringToInt(pAddr); } nRetValue += nLast; return nRetValue; }
脚本功能:
脚本功能其主要目的是能够将用户的操作命令保存成文本,同时也可以从文本中逐行导入命令并执行命令。避免用户的重复操作。其实现也比较简单,就
是将用户输入的所有合法命令添加到一个链表中,在用户调试完一个程序后可以将命令链表中的命令导出到文本文件中。导入功能与之相反,当使用导入功能的时
候,从脚本文件中逐行读取命令文本,通过查全局的“命令-函数对照表”,调用相应的函数。“命令-函数对照表”为如下所示的结构体:
//全局命令-函数对照表 stuCmdNode g_aryCmd[] = { ADD_COMMAND("T", CDoException::StepInto) ADD_COMMAND("P", CDoException::StepOver) ADD_COMMAND("G", CDoException::Run) ADD_COMMAND("U", CDoException::ShowMulAsmCode) ADD_COMMAND("D", CDoException::ShowData) ADD_COMMAND("R", CDoException::ShowRegValue) ADD_COMMAND("BP", CDoException::SetOrdPoint) ADD_COMMAND("BPL", CDoException::ListOrdPoint) ADD_COMMAND("BPC", CDoException::ClearOrdPoint) ADD_COMMAND("BH", CDoException::SetHardPoint) ADD_COMMAND("BHL", CDoException::ListHardPoint) ADD_COMMAND("BHC", CDoException::ClearHardPoint) ADD_COMMAND("BM", CDoException::SetMemPoint) ADD_COMMAND("BML", CDoException::ListMemPoint) ADD_COMMAND("BMC", CDoException::ClearMemPoint) ADD_COMMAND("LS", CDoException::LoadScript) ADD_COMMAND("ES", CDoException::ExportScript) ADD_COMMAND("SR", CDoException::StepRecord) ADD_COMMAND("H", CDoException::ShowHelp) {"", NULL} //最后一个空项 };
其中的ADD_COMMAND为一个宏定义:
#define ADD_COMMAND(str, memberFxn) {str, memberFxn},
简单地说,也就是一个字符串对应一个函数指针,通过命令字符串查对应的函数指针,调用函数。
单步记录指令功能:
单步记录指令就是让程序以单步(步入或步过)的方式运行,将指令地址EIP、对应的二进制指令和一些其他的信息放到一个平衡二叉树(以下简称
AVL树)上,单步运行的过程中,不断地比较当前指令的EIP和二进制指令是否已经存在于AVL树上,如果不存在则在AVL树上添加这个新的指令结点。这
里之所有要用AVL树是出于对检查重复时效率的要求。当然AVL树记录指令非常占用堆空间,如果堆空间消耗严重,可以将AVL树上的一部分内容放到文件中
去。记录指令的意义在于让程序走不同的流程,然后可以比较不同流程的差异。另外也可以使用记录指令的方式跳过无意义的跳转,只让程序记录有意义的指令。
记录指令的过程中,我的做法是遇到CALL一个DLL的导出函数时,就采用单步步过的方式,否则就采用单步步入的方式。实际运用中,对于控制台
程序记录很有效,但是对于基于消息的窗口程序,由于窗口回调函数是系统API在调用的,所以需要先在回调函数中设置断点,然后再记录指令才能记录到消息函
数的代码。
行文至此,我的调试器也讲的差不多了。最后要感谢我在科锐学习以来,钱老师及其他各位老师的教导,同时也要感谢我的同班同学在学习的过程中给予我的帮助。另外要感谢看雪论坛,我的很多思路和想法都源于在看雪论坛上读到的好文章。
本系列文章参考书目、资料如下:
1.《加密与解密3》 编著:段钢
2.《调试寄存器(DRx)理论与实践》 作者:Hume/冷雨飘心
3.《数据结构》 作者:严蔚敏