title: Notes on Hardware Breakpoints and Watchpoints ...
Hardware breakpoints on amd64 are fairly well documented, they are controlled by registers DR0 through DR7.
Register DR7 is a control register, that determines which hardware breakpoints/watchpoints are enabled and how they work. DR6 is a status register will tell you which breakpoint/watchpoint was triggered. Registers DR0 through DR5 contain the address of each breakpoint or watchpoint, DR4 and DR5 are reserved for the kernel.
Precise documentation on the layout of DR7 and DR6 can be found in "IA-32 Architectures Software Developer's Manual, Vol. 3B, section 17.2".
- Write the address in one of DR0, DR1, DR2 or DR3 that isn't being used
- Write the four bits representing length and mode for the breakpoint or watchpoint to DR7
- Write the enable bit for the breakpoint or watchpoint to DR7
- Check the relative bit in DR6, it is the responsibility of the debugger to clear this bit before restarting
On linux the debug registers are stored in the user struct which can be read and written using PTRACE_PEEKUSER and PTRACE_POKEUSER. To determine the offsets for the debug registers you can either use the definition of the user struct, in source/arch/x86/kernel/ptrace.c or just use the constant 848:
ptrace(PEEK_USER, pid, (void *)848, &v);
reads the value of DR0 into v0.
Unlike on windows all 8 registers are present in the user struct, reading DR4 and DR5 (which are reserved for kernel use) will always get you 0x0. Trying to write DR4 and DR5 will result in an error (EIO).
A watchpoint or hardware breakpoint hit is reported with a SIGTRAP, checking the value of DR6 will determine if it was a watchpoint or hardware breakpoint as well as determine which one it was.
DR0 through DR3, DR6 and DR7 are fields in the CONTEXT struct that can be read with GetThreadContext and written with SetThreadContext.
Arm64 is a far bit more complicated than amd64 as well as being overall less documented, especially when it comes to the interfaces provided by the linux kernel.
On arm64 there can be a maximum of 16 watchpoints and 16 hardware breakpoints, however specific implementations of arm64 can (and often will) offer fewer.
All of the following can be referenced in "ARM - Architecture Reference Manual Armv8, for A-profile architectures", the number in parenthesis will be the section number.
The first register of interest is ID_AA64DFR0_EL1 (AArch64 Debug Feature Register 0) which will tell you how many watchpoint and hardware breakpoints exist in your particular CPU (D13.2.59).
Each breakpoint is described by a pair of registers:
- DBGBCRn_EL1 (where n = 0 .. 16) is the breakpoint control register (D13.3.2)
- DBGBVRn_EL1 (where n = 0 .. 16) is the breakpoint value register, which will contain the address of the instruction at which the breakpoint should trigger. (D13.3.3)
The same goes for watchpoints which are described by
- DBGWCRn_EL1 (where n = 0 .. 16) which determines if the watchpoint is enabled, if it is a read or write breakpoint and the length of the data (D13.3.11)
- DBGWVRn_EL1 (where n = 0 .. 16) which contains the address of the watchpoint (D13.3.10)
When a breakpoint or watchpoint is hit the address that caused the breakpoint to trigger is saved in FAR_EL1 the Fault Address Register, which should be checked to determine which breakpoint was triggered.
Because of certain hardware limitations watchpoint hits are imprecise, fortunately details about this are handled by the kernel.
The debug registers are accessed through the PTRACE_GETREGSET/PTRACE_SETREGSET interface, when reading register set types NT_ARM_HW_WATCH and NT_ARM_HW_BREAK.
The layout of the register set is described by the structure user_hwdebug_state defined in arch/arm64/include/uapi/asm/ptrace.h, and is as follows:
- 1byte: number of watchpoints for NT_ARM_HW_WATCH or breakpoints for NT_ARM_HW_BREAK
- 1byte: debug architecture version (the 4 least significant bits of ID_AA64DFR0_EL1)
- 6bytes: padding
then for each watchpoint (or breakpoint) a pair of 64bit words follow, the first one being the "value" register (DBGBVRn_EL1 or DBGWVRn_EL1) and the second one being the control register (DBGBCRn_EL1 or DBGWCRn_EL1).
Note that for the control register of watchpoints (DBGWCRn_EL1) only the fields BAS, LSC, PAC and E are considered, which greatly reduces the features of arm64 which you are able to use.
When reading with PTRACE_GETREGSET you can just read all 16 registers, even if the CPU defines fewer. The registers that don't exist will simply be always zeroed.
However when writing with PTRACE_SETREGSET you can only write registers that exist, even if you simply pass 0x0 for the ones that don't. Trying to write non-existent registers will return the slightly confusing ENOSPC.
When a breakpoint or watchpoint is triggered a SIGTRAP will be returned to the debugger by waitpid. To determine if the cause of the SIGTRAP was actually a watchpoint use PTRACE_GETSIGINFO to retrieve the siginfo_t struct for each thread that received SIGTRAP and check that the Code is 0x4 (TRAP_HWBKPT), the addr field of siginfo_t will contain the address that caused the watchpoint hit (i.e. the address that was saved in FAR_EL1 by the CPU).