This article mainly analyzes the process of adding hardware breakpoints and software breakpoints in openocd.
Hardware Breakpoints#
if (breakpoint->type == BKPT_HARD) {
int64_t bpt_value;
while (brp_list[brp_i].used && (brp_i < aarch64->brp_num))
brp_i++;
if (brp_i >= aarch64->brp_num) {
LOG_ERROR("ERROR Can not find free Breakpoint Register Pair");
return ERROR_TARGET_RESOURCE_NOT_AVAILABLE;
}
breakpoint_hw_set(breakpoint, brp_i);
if (breakpoint->length == 2)
byte_addr_select = (3 << (breakpoint->address & 0x02));
control = ((matchmode & 0x7) << 20)
| (1 << 13)
| (byte_addr_select << 5)
| (3 << 1) | 1;
brp_list[brp_i].used = 1;
brp_list[brp_i].value = breakpoint->address & 0xFFFFFFFFFFFFFFFCULL;
brp_list[brp_i].control = control;
bpt_value = brp_list[brp_i].value;
retval = aarch64_dap_write_memap_register_u32(target, armv8->debug_base
+ CPUV8_DBG_BVR_BASE + 16 * brp_list[brp_i].brpn,
(uint32_t)(bpt_value & 0xFFFFFFFF));
if (retval != ERROR_OK)
return retval;
retval = aarch64_dap_write_memap_register_u32(target, armv8->debug_base
+ CPUV8_DBG_BVR_BASE + 4 + 16 * brp_list[brp_i].brpn,
(uint32_t)(bpt_value >> 32));
if (retval != ERROR_OK)
return retval;
retval = aarch64_dap_write_memap_register_u32(target, armv8->debug_base
+ CPUV8_DBG_BCR_BASE + 16 * brp_list[brp_i].brpn,
brp_list[brp_i].control);
if (retval != ERROR_OK)
return retval;
LOG_DEBUG("brp %i control 0x%0" PRIx32 " value 0x%" TARGET_PRIxADDR, brp_i,
brp_list[brp_i].control,
brp_list[brp_i].value);
}
The implementation of hardware breakpoints mainly relies on two registers, CPUV8_DBG_BCR_BASE
and CPUV8_DBG_BVR_BASE
.
CPUV8_DBG_BCR_BASE
is the breakpoint control register, and you can see that the second-to-last bit of the address is stored in it.
if (breakpoint->length == 2)
byte_addr_select = (3 << (breakpoint->address & 0x02));
control = ((matchmode & 0x7) << 20)
| (1 << 13)
| (byte_addr_select << 5)
| (3 << 1) | 1;
CPUV8_DBG_BVR_BASE
is the breakpoint data register, and you can see that its value depends on the BT of the control register. In openocd, the corresponding BT is 0, and the data register stores the virtual address.
From the perspective of the code, the value stored in the data register is: breakpoint->address & 0xFFFFFFFFFFFFFFFCULL;
, which is the breakpoint address except for the last two bits.
Software Breakpoints#
if (breakpoint->type == BKPT_SOFT) {
uint32_t opcode;
uint8_t code[4];
if (armv8_dpm_get_core_state(&armv8->dpm) == ARM_STATE_AARCH64) {
LOG_INFO("state ARM_STATE_AARCH64");
opcode = ARMV8_HLT(11);
if (breakpoint->length != 4)
LOG_ERROR("bug: breakpoint length should be 4 in AArch64 mode");
} else {
/**
* core_state is ARM_STATE_ARM
* in that case the opcode depends on breakpoint length:
* - if length == 4 => A32 opcode
* - if length == 2 => T32 opcode
* - if length == 3 => T32 opcode (refer to gdb doc : ARM-Breakpoint-Kinds)
* in that case the length should be changed from 3 to 4 bytes
**/
opcode = (breakpoint->length == 4) ? ARMV8_HLT_A1(11) :
(uint32_t) (ARMV8_HLT_T1(11) | ARMV8_HLT_T1(11) << 16);
if (breakpoint->length == 3)
breakpoint->length = 4;
}
LOG_INFO("opcode:0x%X",opcode);
buf_set_u32(code, 0, 32, opcode);
retval = target_read_memory(target,
breakpoint->address & 0xFFFFFFFFFFFFFFFEULL,
breakpoint->length, 1,
breakpoint->orig_instr);
if (retval != ERROR_OK)
return retval;
armv8_cache_d_inner_flush_virt(armv8,
breakpoint->address & 0xFFFFFFFFFFFFFFFEULL,
breakpoint->length);
retval = target_write_memory(target,
breakpoint->address & 0xFFFFFFFFFFFFFFFEULL,
breakpoint->length, 1, code);
if (retval != ERROR_OK)
return retval;
armv8_cache_d_inner_flush_virt(armv8,
breakpoint->address & 0xFFFFFFFFFFFFFFFEULL,
breakpoint->length);
armv8_cache_i_inner_inval_virt(armv8,
breakpoint->address & 0xFFFFFFFFFFFFFFFEULL,
breakpoint->length);
breakpoint->is_set = true;
}
The principle of software breakpoints is to use HLT to replace the original instruction at the breakpoint location. When the program reaches this address, a halt exception will be triggered. At this time, jlink takes over the program and restores the breakpoint instruction to the original instruction, which is basically the same as the gdbstub principle.
The difference is that the triggering exception instruction used is not HLT, and the reading and writing of memory is forwarded through the debug register.
The problem with the software breakpoints encountered earlier was caused by an error in restoring the original instruction.