[调试器实现]第五章 单步异常的处理
因为在调试器的设计与实现中,很多关键性的操作都是在单步异常处理中完成的,故本章重点论述在单步异常中的处理。首先我们来看看会有哪些情况导致调试器进入单步异常。
进入单步异常的原因:
1. 用户输入了单步进入的命令,调试器需要设置单步,让被调试程序单步执行。
2. 用户所下的INT3断点被断下后,调试器会暂时恢复INT3断点处的字节为原有的字节,并让被调试线程的EIP减一,为了重新设置这个INT3断点,调试器自己设置了单步。
3. 用户所下的硬件断点被断下时,会触发单步异常。
4. 用户所下的硬件执行断点被断下后,调试器会暂时取消掉该硬件执行断点,以便被调试进程顺利跑下去,为了重新设置该硬件执行断点,调试器自己设置了单步。
5. 用户所下的内存断点被断下后,调试器会暂时恢复产生访问异常的内存分页为原来的属性,以便被调试进程顺利跑下去,为了重新设置该内存分页的属性(以便内存断点继续起作用),调试器自己设置了单步。
单步异常的处理:
从以上所述各点来看,进入单步的原因有三种,一是用户需要单步步入运行程序;二是调试器需要重新设置之前被临时取消的断点而设置了单步;三是硬件断点被断下时触发的单步。当然也有可能几种原因同时存在。所以我们需要几个BOOL变量来表明是否有需要重设的断点。INT3断点对应一个BOOL变量,硬件执行断点对应一个BOOL变量,是否是用户输入的单步步入命令对应一个BOOL变量。另外,进入单步后还需要检查线程环境中的Dr6的低4位是否有值为1的位,如果有那么进入单步的原因之一是因为触发了硬件断点,此时需要进一步判断该硬件断点是否是硬件执行断点,如果是硬件执行断点需要做相应的处理(具体处理方法见《调试器实现(第三章)硬件断点》)。
多断点重合的情况:
当用户对代码段的同一个地址(指令首字节)即设置了硬件执行断点,又设置了INT3断点,同时还设置了内存访问断点,此时会先触发硬件执行断点,然后会触发内存访问断点,最后会触发INT3断点。如果用户不想在同一个地址被多个断点断下多次,可以在相应的异常中做判断,先临时取消掉同一地址处的其他类型的断点,然后设置一个单步,进入单步后再把前面取消的断点再重新设置上。
处理单步异常的模块代码:
//处理单步异常
BOOL CDoException::DoStepException()
{
BOOL bRet;
DWORD dwDr6 = 0; //硬件调试寄存器Dr6的值
DWORD dwDr6Low = 0; //硬件调试寄存器Dr6低4位的值
stuPointInfo tempPointInfo;
stuPointInfo* pResultPointInfo = NULL;
char CodeBuf[24] = {0};
UpdateContextFromThread();
//是否需要重设INT3断点
if (m_isNeedResetPoint == TRUE)
{
m_isNeedResetPoint = FALSE;
char chCC = (char)0xcc;
bRet = WriteProcessMemory(m_hProcess, m_pFindPoint->lpPointAddr,
&chCC, 1, NULL);
if (bRet == FALSE)
{
printf("WriteProcessMemory error!\r\n");
return FALSE;
}
}
//是否需要重设硬件断点
if (m_isNeedResetHardPoint == TRUE)
{
m_Context.Dr7 |= (int)pow(4, m_nNeedResetHardPoint);
UpdateContextToThread();
m_isNeedResetHardPoint = FALSE;
}
dwDr6 = m_Context.Dr6;
dwDr6Low = dwDr6 & 0xf; //取低4位
//如果是由硬件断点触发的单步,需要用户输入才能继续
//另外,如果是硬件执行断点,则需要先暂时取消断点,设置单步,下次再恢复断点
if (dwDr6Low != 0)
{
ShowHardwareBreakpoint(dwDr6Low);
m_nNeedResetHardPoint = log(dwDr6Low)/log(2)+0.5;//加0.5是为了四舍五入
//判断由 dwDr6Low 指定的DRX寄存器,是否是执行断点
if((m_Context.Dr7 << (14 - (m_nNeedResetHardPoint * 2))) >> 30 == 0)
{
switch (m_nNeedResetHardPoint)
{
case 0:
m_Context.Dr7 &= 0xfffffffe;
break;
case 1:
m_Context.Dr7 &= 0xfffffffb;
break;
case 2:
m_Context.Dr7 &= 0xffffffef;
break;
case 3:
m_Context.Dr7 &= 0xffffffbf;
break;
default:
printf("Error!\r\n");
}
m_Context.EFlags |= TF;
UpdateContextToThread();
m_isNeedResetHardPoint = TRUE;
}
m_isUserInputStep = TRUE; //这个设置只是为了能够等待用户输入
}
if (m_isUserInputStep == FALSE)
{
//重设内存断点
ResetMemBp();
return TRUE;
}
//以下代码在用户输入为 "T" 命令、或硬件断点触发时执行
//如果此处有INT3断点,则需要先暂时删除INT3断点
//这样做是为了在用户输入“T”命令、或硬件断点触发时忽略掉INT3断点
//以免在一个地方停下两次
memset(&tempPointInfo, 0, sizeof(stuPointInfo));
tempPointInfo.lpPointAddr = m_DbgInfo.ExceptionRecord.ExceptionAddress;
tempPointInfo.ptType = ORD_POINT;
if (FindPointInList(tempPointInfo, &pResultPointInfo, TRUE))
{
//非一次性断点,才需要重设断点
if (pResultPointInfo->isOnlyOne == FALSE)
{
m_Context.EFlags |= TF;
UpdateContextToThread();
m_isNeedResetPoint = TRUE;
}
else//一次性断点,从链表里面删除
{
delete[] m_pFindPoint;
g_ptList.erase(m_itFind);
}
WriteProcessMemory(m_hProcess, m_pFindPoint->lpPointAddr,
&(m_pFindPoint->u.chOldByte), 1, NULL);
if (bRet == FALSE)
{
printf("WriteProcessMemory error!\r\n");
return FALSE;
}
}
m_lpDisAsmAddr = m_DbgInfo.ExceptionRecord.ExceptionAddress;
m_isUserInputStep = FALSE;
//更新m_Context为现在的环境值
UpdateContextFromThread();
//显示汇编代码和寄存器信息
ShowAsmCode();
ShowRegValue(NULL);
//重设内存断点
ResetMemBp();
//等待用户输入
bRet = FALSE;
while (bRet == FALSE)
{
bRet = WaitForUserInput();
}
return TRUE;
}
本系列文章参考书目、资料如下:
1.《加密与解密3》 编著:段钢
2.《调试寄存器(DRx)理论与实践》 作者:Hume/冷雨飘心
3.《数据结构》 作者:严蔚敏

浙公网安备 33010602011771号