陷阱标志
IA-32处理器支持的调试陷阱标志共有3种。
1. 8086支持的单步执行标志(EFLAGS的TF位)。
2. 386引入的任务状态陷阱标志(TSS的T标志)。
3. 奔腾Pro引入的分支到分支单步执行标志(DebugCtl寄存器种的BTF标志)。
1.单步执行标志
标志寄存器(FLAGS)的TF(Trap Flag)位。当TF为1时,CPU每执行完一条指令便会产生一个调试异常(#DB),中断到调试异常处理程序,调试器的单步执行功能大多是依靠这一机制来实现的。
调试异常的向量号是1,设置TF标志会导致CPU每执行一条指令后都转区执行1号异常的处理例程。为方便,把因TF标志触发的调试异常称为单步异常(single-step exception)。
单步异常属于陷阱类异常,CPU总是在执行完导致此类异常的指令后才产生该异常。这说明当因单步异常中断到调试器时,导致该异常的指令已经执行完毕了。
软件断点异常(#BP)和硬件断点中的数据及I/O断点异常也是陷阱类异常。
硬件断点的指令访问异常是错误类异常,当由于此异常而中断到调试器时,相应调试地址寄存器(DRn)中所指地址处的指令还未执行。
CPU是何时检查TF标志的呢 ?
IA-32手册原文是“while an instruction is being executed”。推测应该是一条指令即将执行完毕的时候。即,当CPU即将执行完一条指令时检测TF位,如果为1,那么CPU先清除此位,然后准备产生异常。
例外情况,如POPF等可以设置TF位的指令,CPU不会检测TF位,即不会立刻产生单步异常,而是其后的下一条指令将产生单步异常。
因为CPU在进入异常处理例程前会自动清除TF标志,所以当CPU中断到调试器中后再观察TF标志,它的值总是0。
调试异常的向量号是1,不可以像INT 3指令实现手工断点。因为系统对INT 3 指令特别对待,允许其在用户模式下执行。
INT 1机器码为0xCD01,是普通的INT n指令。在保护模式下,如果执行INT n指令时当前的CPL大于引用的门描述符的DPL,就回产生通用保护异常(#GP)。
2.任务状态段陷阱标志
386引入的调试陷阱标志,任务状态段(Task-State Segment,TSS) 的T标志。在TSS中,字节偏移为100的16位字(word)的最低位是调试陷阱标志位,简称T标志。
如果T标志被置为1,CPU在将程序控制权转移到新的任务但还没有开始执行新任务的第一条指令时产生异常的。
3.按分支单步执行标志
按分支单步执行(Branch Trace Flag,BTF),就是每步进(step)一次,CPU会一直执行到有分支、中断或异常发生。
如何启用按分支单步执行?
同时置起EFLAGES 的TF和MSR(Model Specific Register)的BTF位。
用代码进行说明:
#define DEBUGCTRL_MSR 0x1D9
#define BTF 2
int main(int argc, char* argv[])
{
int m, n;
MSR_STRUCT msr;
if (!EnablePrivilege(SE_DEBUG_NAME)
|| !EnsureVersion(5, 1)
|| !GetSysDbgAPI())
{
printf("Failed in initialization.\n");
return E_FAIL;
}
memset(&msr, 0, sizeof(MSR_STRUCT));
msr.MsrNum = DEBUGCTRL_MSR;
msr.MsrLo |= BTF;
WriteMSR(msr);//用来启用按分支单步执行功能,即设置BTF标志
//以下代码将全速运行
m = 10, n = 2;
m = n * 2 - 1;
if (m == m * m / m)
m = 1;
else
{
m = 2;
}
//按F10单步执行,一次可以单步到这里
m *= m;
if (ReadMSR(msr))
{
PrintMsr(msr);
}
else
printf("Failed to ReadMSR().\n");
return S_OK;
}
先在第22行设置一个断点,然后按F5快捷键运行到这个断点位置。第19行是用来启用按分支单步执行功能的,即设置起BTF标志。接下来,我们按F10快捷键单步执行,会发现一下子会执行到第31行,即从第22行单步一次就执行到了第31行,这便是按分支单步执行的效果。
为什么会执行到第31行呢?
按照分支到分支单步执行的定义,CPU会在执行到下一次跳转发生时停止。对于例子,CPU在执行第22行对应的第一条汇编指令时,CPU会检测到TF标志和BTF标志,当TF和BTF标志同时被置起时,CPU会认为当前是在按分支单步执行,所以会判断是否有跳转发生。
需要解释一下,这里所说的有跳转发生,是指执行当前指令的结果导致程序指针的值发生了跳跃,是与顺序执行的逐步递增相对而言的。值得说明的是,如果当前指令是条件转移指令(比如JA、JAE、JNE等),而且转移条件不满足,那么是不算有跳转发生的,CPU仍会继续执行。
在例子中,因为第22行的第一条汇编指令根本不是分支指令,所以CPU会继续执行。以此类推,CPU会连续执行到第24行的if语句对应的最后一条汇编指令jne。因为这条语句是条件转移语句而且转移条件满足,所以执行这条指令会导致程序指针跳越。当CPU在执行这条指令的后期检查TF和BTF标志时,会认为已经满足产生异常的条件,在清除TF和BTF标志后,就产生单步异常中断到调试器。因为EIP总是指向即将要执行的指令,所以VS会将当前位置设到第31行,而不是第24行。也就是说,中断到调试器时,分支语句已经执行完毕,但是跳转到的那条语句还没有执行。
第24行的汇编代码:
1 128: if(m==m*m/m)
2 0040DBBB 8B 45 FC mov eax,dword ptr [ebp-4]
3 0040DBBE 0F AF 45 FC imul eax,dword ptr [ebp-4]
4 0040DBC2 99 cdq
5 0040DBC3 F7 7D FC idiv eax,dword ptr [ebp-4]
6 0040DBC6 39 45 FC cmp dword ptr [ebp-4],eax
7 0040DBC9 75 09 jne main+0B4h (0040dbd4)
如果在从第22行执行到第24行的过程中,有中断或异常发生,那么CPU也会认为停止单步执行的条件已经满足。因此,按分支单步执行的全称是按分支、异常和中断单步(single-step on branches, exceptions and interrupts)执行。
在WinDBG调试器调试时,执行tb命令便可以按分支单步跟踪。但是当调试WoW64程序(运行在64位Windows系统中的32位应用程序)时,这条命令是不工作的,WinDBG显示Operation not supported by current debuggee error in 'tb'(当前的被调试目标不支持此操作)。另外,因为需要CPU的硬件支持,在某些虚拟机里调试时,WinDBG也会显示这样的错误提示。
参考:软件调试-硬件基础