软件调试之硬件断点
硬件断点
1.调试寄存器
IA-32处理器定义了8个调试寄存器,分别为DR0~DR7。在32位模式下,它们是32位的;在64位模式下,它们是64位的。如下图:
DR4、DR5 是保留的。
4个32位调试地址寄存器(DR0~DR3),64位下是64位的。
1个32位调试控制器(DR7),64位时,高32位保留未用。
1个32位的调试状态寄存器(DR6),64位时,高32位保留未用。
最多可以设置4个断点。其分工是DR0~DR3用来指定断点的内存(线性地址)或I/O地址。DR7定义断点的中断条件。当调试事件发生时,DR6向调试器报告事件的详细信息,以供调试器判断发生的是何种事件。
2.调试地址寄存器
设置在内存中间的断点,这个地址是断点的线性地址而不是物理地址,因为CPU是在线性地址被翻译为物理地址之前来做断点匹配工作的。
在保护模式下,不能针对一个物理内存地址设置断点。
3.调试控制寄存器
DR7有24位被划分成4组分别与4个调试地址寄存器相对应,如L0、G0、R/W0、LEN0这6位与DR0相对应,以此类推。
DR7各个位域的含义,如下表:
简称 |
全称 |
比特位 |
描述 |
R/W0~R/W3 |
读写域 |
R/W0:16,17 R/W1:20,21 R/W2:24,25 R/W3:28,29 |
分别与DR0~DR3这4个调试地址寄存器相对应, 用来指定被监控地址的访问类型,其含义如下。 ● 00:仅当执行对应地址的指令时中断 ● 01:仅当向对应地址写数据时中断 ● 10:386和486不支持此组合。对于以后的CPU,可以通过把CR4寄存器的DE(调试扩展)位设为1启用该组合,其含义为“当向相应地址进行输入输出(即I/O读写)时中断” ● 11:当向相应地址读写数据时都中断,但是从该地址读取指令除外 |
LEN0~LEN3 |
长度域 |
LEN0:18,19 LEN1:22,23 LEN2:26,27 LEN3:30,31 |
分别与DR0~DR3这4个调试地址寄存器相对应,用来指定要监控的区域长度,其含义如下。 ● 00:1字节长 ● 01:2字节长 ● 10:8字节长(奔腾4或至强CPU)或未定义(其他处理器) ● 11:4字节长 注意:如果对应的R/Wn为0(即执行指令中断),那么这里的设置应该为0,参见下文 |
L0~L3 |
局部断点启用 |
L0:0 L1:2 L2:4 L3:6 |
分别与DR0~DR3这4个调试地址寄存器相对应,用来启用或禁止对应断点的局部匹配。如果该位设为1,当CPU在当前任务中检测到满足所定义 的断点条件时便中断,并且自动清除此位。如果该位设为0,便禁止此断点 |
G0~G3 |
全部断点启用 |
G0:1 G1:3 G2:5 G3:7 |
分别对应DR0~DR3这4个调试地址寄存器,用来全局启用和禁止对应的断点。如果该位设为1,当CPU在任何任务中检测到满足所定义的断点条 件时都会中断;如果该位设为0,便禁止此断点。与L0~L3不同,断点条件发生时,CPU不会自动清除此位 |
LE和GE |
启用局部或者全局(精确)断点(Local and Global (exact) breakpointEnable) |
LE:8 GE:9 |
从486开始的IA-32处理器都忽略这两位的设置。此前这两位是用来启用或禁止数据断点匹配的。对于早期的处理器,当设置有数据断点时,需要启用本设置,这时CPU会降低执行速度,以监视和保证当有指令要访问符合断点条件的数据时产生调试异常 |
GD |
启用访问检测 (General Detect Enable) |
13 |
启用或禁止对调试寄存器的保护。当设为1时,如果CPU检测到将修改调试寄存器(DR0~DR7)的指令,CPU会在执行这条指令前产生一个调试异常 |
读写域R/Wn ,占两个二进制位,可以指定4种访问方式。通过设置读写域,可以指定断点的访问类型。以下是3类典型的读写域使用方式:
- 读/写内存中的数据时中断:又称为数据访问断点(data access breakpoint)。利用数据访问断点,可以监控对全局变量或局部变量的读写操作。以WinDBG 为例,对内存区进行写操作时中断,ba w4 00401200,ba 代表break on access, w 表示写(write),4 表示4字节。对内存区进行读操作中断,ba r4 00401200,r 表示读(read)。
- 执行内存中的代码时中断:又称为代码访问断点(code access breakpoint)或指令断点(instruction breakpoint)
- 读写I/O(输入输出)端口时中断:又称为I/O访问断点(Input/Output access breakpoint)。
LENn(n=0,1,2,3,位于DR7中)位段可以指定1、2、4或8字节长的范围。
代码访问断点,长度域应为00,表示1字节长度。
数据和I/O访问断点,有两点需要注意:
- 只要断点区域中的任一字节在被访问的范围内,都会触发该断点。
- 边界对齐要求,2字节区域必须按字(word)边界对齐,4字节区域必须按双字(doubleword)边界对齐,8字节区域必须按4字(quadword)边界对齐。也就是说,CPU在检查断点匹配时会自动去除相应数量的低位。如果地址没有按要求对齐,可能无法实现预期的结果。例如,假设希望通过将DR0设为0xA003、将LEN0设为11(代表4字节长)实现任何对0xA003~0xA006内存区的写操作都会触发断点,那么只有当0xA003被访问时会触发断点,对0xA004、0xA005和0xA006处的内存访问都不会触发断点。因为长度域指定的是4字节,所以CPU在检查地址匹配时,会自动屏蔽起始地址0xA003的低2位,只是匹配0xA000。而0xA004、0xA005和0xA006屏蔽低2位后都是0xA004,所以无法触发断点。
4.指令断点
指令断点说明,代码片段:
MOV SS, EAX
MOV ESP, EBP
如果断点被设置在紧邻MOV SS EAX 的下一行,那么该断点永远不会被触发。原因时为了保护栈寄存器(SS) 和栈顶指针(ESP)的一致性,CPU执行MOV SS 指令时会禁止所有中断和异常,直到执行完下一条指令。
类似的有POP SS 指令的下一条指令处的指令断点也不会被触发。
POP SS
POP ESP
LSS 指令来加载SS和ESP寄存器,通过LSS指令可以改变SS和ESP两个寄存器。
5.调试异常
IA-32架构分配了两个中断向量来支持软件调试,即向量1和向量3。向量3 用于INT 3指令产生的断点异常(breakpoint exception,即#BP)。向量1 用于其他情况的调试异常,简称调试异常(debug exception ,即#DB)。硬件断点产生的是调试异常,CPU会执行1号向量所对应的处理例程。
导致调试异常的各种情况,如下表:
异常情况 |
DR6标志 |
DR7标志 |
异常类型 |
因为EFlags[TF]=1而导致的单步异常 |
BS=1 |
|
陷阱 |
调试寄存器DRn和LENn定义的指令断点 |
Bn=1 and (Gn=1 or Ln=1)
|
R/Wn=0 |
错误 |
调试寄存器DRn和LENn定义的写数据断点 |
Bn=1 and (Gn=1 or Ln=1) |
R/Wn=1 |
陷阱 |
调试寄存器DRn和LENn定义的I/O读写断点 |
Bn=1 and (Gn=1 or Ln=1) |
R/Wn=2 |
陷阱 |
调试寄存器DRn和LENn定义的数据读(不包括 取指)写断点 |
Bn=1 and (Gn=1 or Ln=1) |
R/Wn=3 |
陷阱 |
当DR7的GD位为1时,企图修改调试寄存器 |
BD=1 |
|
错误 |
任务状态段(TSS)的T标志为1时进行任务切换 |
BT=1 |
|
陷阱 |
对于错误类调试异常,因为恢复执行后断点条件仍然存在,所以为了避免反复发生异常,调试软件必须在使用IRETD指令返回重新执行触发异常的指令前将标志寄存器的RF(Resume Flag)位设为1,告诉CPU不要在执行返回后的第一条指令时产生调试异常,则CPU执行完该条指令后会自动清除RF标志。
6.调试状态寄存器
调试状态寄存器(DR6)的作用是当CPU检测到匹配断点条件的断点或有其他调试事件发生时,用来向调试器的断点异常处理程序传递断点异常的详细信息,以便使调试器可以很容易地识别出发生的是什么调试事件。例如,如果B0被置为1,那么就说明满足DR0、LEN0和R/W0所定义条件的断点发生了。
调试状态寄存器(DR6)各个标志位具体含义,如下表:
简称 |
全称 |
比特位 |
描述 |
B0 |
Breakpoint 0 |
0 |
如果处理器检测到满足断点条件0的情况,那么处理器会在调用异常处理程序前将此位置为1 |
B1 |
Breakpoint 1 |
1 |
如果处理器检测到满足断点条件1的情况,那么处理器会在调用异常处理程序前将此位置为1 |
B2 |
Breakpoint 2 |
2 |
如果处理器检测到满足断点条件2的情况,那么处理器会在调用异常处理程序前将此位置为1 |
B3 |
Breakpoint 3 |
3 |
如果处理器检测到满足断点条件3的情况,那么处理器会在调用异常处理程序前将此位置为1 |
BD |
检测到访问调试寄存器 |
13 |
这一位与DR7的GD位相联系,当GD位被置为1,而且CPU发现了要修改调试寄存器(DR0~DR7)的指令时,CPU会停止继续执行这条指令,把BD位设为1,然后把执行权交给调试异常(#DB)处理程序 |
BS |
单步(Single step) |
14 |
这一位与标志寄存器的TF位相联系,如果该位为1,则表示异常是由单步执行(single step)模式触发的。与导致调试异常的其他情况相比,单步情况的优先级最高,因此当此标志为1时,也可能有其他标志也为1 |
BT |
任务切换(Task switch) |
15 |
这一位与任务状态段(TSS)的T标志(调试陷阱标志,debugtrap flag)相联系。当CPU在进行任务切换时,如果发现下一个任务的TSS的T标志为1,则会设置BT位,并中断到调试中断处理程序 |
7.示例
硬件断点示例加深理解。
编号 |
地址寄存器 |
R/Wn |
LENn |
断点触发条件 |
0 |
DR0=A0001H |
R/W0=11(读/ 写) |
LEN0=00(1B) |
读写A0001H开始的1字节 |
1 |
DR1=A0002H |
R/W1=01(写) |
LEN1=00(1B) |
写A0002H开始的1字节 |
2 |
DR2=B0002H |
R/W2=11(读/ 写) |
LEN2=00(2B) |
读写B0002H开始的2字节 |
3 |
DR3=C0000H |
R/W3=01(写) |
LEN3=11(4B) |
写C0000H开始的4字节 |
内存访问示例:
访问类型 |
访问地址 |
访问长度 |
触发断点与否 |
读或写 |
A0001H |
1 |
触发(与断点0匹配) |
读或写 |
A0001H |
2 |
触发(读与断点0匹配,写与断点0和1都匹配) |
写 |
A0002H |
1 |
触发(与断点1匹配) |
写 |
A0002H |
2 |
触发(与断点1匹配) |
读或写 |
B0001H |
4 |
触发(与断点2匹配,对B0002和B0003的访问落入断点2定义的区域) |
读或写 |
B0002H |
1 |
触发(与断点2匹配) |
读或写 |
B0002H |
2 |
触发(与断点2匹配) |
写 |
C0000H |
4 |
触发(与断点3匹配) |
写 |
C0001H |
2 |
触发(与断点3匹配) |
写 |
C0003H |
1 |
触发(与断点3匹配) |
读或写 |
A0000H |
1 |
否 |
读 |
A0002H |
1 |
否(断点1的访问类型是写) |
读或写 |
A0003H |
4 |
否 |
读或写 |
B0000H |
2 |
否 |
读 |
C0000H |
2 |
否(断点3的访问类型是写) |
读或写 |
C0004H |
4 |
否 |
8. 硬件断点的设置方法
VS 设置硬件断点:
/*---------------------------------------------------------------------
// DataBP.cpp : Demonstrate setting data access breakpoint manually.
Software Debugging by Raymond Zhang, All rights reserved.
---------------------------------------------------------------------*/
#include "stdafx.h"
#include <windows.h>
#include <stdlib.h>
int main(int argc, char* argv[])
{
CONTEXT cxt;
HANDLE hThread=GetCurrentThread();//获取当前线程的CONTEXT结构,其中包含了线程的通用寄存器和调试寄存器信息
DWORD dwTestVar=0;
//检查当前程序是否正在被调试
if(!IsDebuggerPresent())
{//如果不是正在被调试,当断点被触发时导致异常错误
printf("This sample can only run within a debugger.\n");
return E_FAIL;
}
cxt.ContextFlags=CONTEXT_DEBUG_REGISTERS|CONTEXT_FULL;
if(!GetThreadContext(hThread,&cxt))
{
printf("Failed to get thread context.\n");
return E_FAIL;
}
cxt.Dr0=(DWORD) &dwTestVar;//将内存地址放入DR0
cxt.Dr7=0xF0001;//4 bytes length read& write breakponits, 设置DR7,F表示4字节读写访问;01 表示启用DR0断点
if(!SetThreadContext(hThread,&cxt)) //使寄存器设置生效
{
printf("Failed to set thread context.\n");
return E_FAIL;
}
dwTestVar=1;//修改内存数据以触发断点
GetThreadContext(hThread,&cxt);
printf("Break into debuger with DR6=%X.\n",cxt.Dr6);
return S_OK;
}
WinDBG 使用ba命令设置硬件断点,如ba w4 0xabcd, CPU一旦再对内存地址0xabcd开始的4字节范围内的任何字节执行写访问,便会产生调试异常。如果把w4换成r4,那么读写这个内存范围都会触发异常。
参考:软件调试-硬件基础