最近安卓项目中碰到一个踩内存导致死机的问题,通过分析Log找到了被踩的内存地址,但无法找到是谁踩掉的。一般踩内存的问题,可以通过下硬件数据断点来找到肇事者。但在这个项目中,踩内存是在安卓开机过程中发生的,来不及上JTAG,另外被踩的内存是动态分配出来的,每次开机都不同(但总是踩在这个被分配的地址的固定字节上),无法预先指定断点地址(如果是全局变量被踩的话,其地址一般是不变的,可以直接在被踩的地址上下数据断点),所以这种情况下只能通过程序来给自己下断点(self-host),程序分配内存后,马上给被踩的内存下个数据断点(已知踩内存肯定发生在这个地址上),当其它程序意外写入该内存时,就会发生异常中断,从而找到踩内存的肇事者。
不幸的是,百度后,几乎没有文章讲这个事情怎么做。翻了GDB的代码,也没看到ARM中如何下硬件断点(所用的GDB版本太旧了)。后来多方搜索,找到一些信息,记录在下:
1. 在ARM的架构文档中(ARM官网可下载),Cortext A8, A9, A15才支持硬件断点(通过协处理器CP14操作调试寄存器DBGWCR和DBGWVR来下数据断点(watchpoint),Processor和JTAG Debuger均可以操作它们)(现在跑安卓的机器一般都是A9以上的架构,所以基本都支持硬件断点)。
2. Linux内核要在2.6.37以后的版本才支持对ARM添加硬件断点。
3. GDB要在7.3以后的版本才支持对ARM添加硬件断点(最新版本是7.8.1,2014年10月)。
如果是自己的ARM系统,可以写汇编,通过MCR协处理器指令操作CP14,从而读写DBGWCR和DBGWVR来下硬件断点。
如果是Linux系统(2.6.37版本以上),就简单了,可以通过ptrace函数来操作DBGWCR和DBGWVR,下硬件断点。
以下是从GDB 7.8.1中截取的下硬件断点的部分代码:
for (i = 0; i < arm_linux_get_hw_breakpoint_count (); i++)
if (arm_lwp_info->bpts_changed[i])
{
errno = 0;
if (arm_hwbp_control_is_enabled (bpts[i].control))
if (ptrace (PTRACE_SETHBPREGS, pid,
(PTRACE_TYPE_ARG3) ((i << 1) + 1), &bpts[i].address) < 0)
perror_with_name (_("Unexpected error setting breakpoint"));
if (bpts[i].control != 0)
if (ptrace (PTRACE_SETHBPREGS, pid,
(PTRACE_TYPE_ARG3) ((i << 1) + 2), &bpts[i].control) < 0)
perror_with_name (_("Unexpected error setting breakpoint"));
arm_lwp_info->bpts_changed[i] = 0;
}
for (i = 0; i < arm_linux_get_hw_watchpoint_count (); i++)
if (arm_lwp_info->wpts_changed[i])
{
errno = 0;
if (arm_hwbp_control_is_enabled (wpts[i].control))
if (ptrace (PTRACE_SETHBPREGS, pid,
(PTRACE_TYPE_ARG3) -((i << 1) + 1), &wpts[i].address) < 0)
perror_with_name (_("Unexpected error setting watchpoint"));
if (wpts[i].control != 0)
if (ptrace (PTRACE_SETHBPREGS, pid,
(PTRACE_TYPE_ARG3) -((i << 1) + 2), &wpts[i].control) < 0)
perror_with_name (_("Unexpected error setting watchpoint"));
arm_lwp_info->wpts_changed[i] = 0;
}
可以看到,调用ptrace,参数使用PTRACE_GETHBPREGS和PTRACE_SETHBPREGS即可读写硬件断点寄存器,其中第3个参数是寄存器的索引号,正数是读写breakpoint寄存器,负数是读写watchpoint寄存器(即数据断点)。+1,+2是读写第1个断点的地址和控制寄存器,+3,+4是读写第2个断点的地址和控制寄存器,以此类推;而-1,-2是读写第1个数据断点的地址和控制寄存器,-3,-4是读写第2个数据断点的地址和控制寄存器,以此类推。如果第3个参数为0,则表示读取调试信息,例如支持多少个硬件断点,多少个数据断点等。
查看linux kernel 3.1.10的代码,看看ptrace的实现,在3.1.10/kernel/ptrace.c中是ptrace的代码:
SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr, unsigned long, data)
它会调用架构相关的arch_ptrace函数实现调试功能,在ARM中其实现在3.1.10/arch/arm/kernel/ptrace.c中:
long arch_ptrace(struct task_struct *child, long request, unsigned long addr, unsigned long data)
在此函数中,对PTRACE_GETHBPREGS和PTRACE_SETHBPREGS分别调用ptrace_gethbpregs和ptrace_sethbpregs函数:
#ifdef CONFIG_HAVE_HW_BREAKPOINT
case PTRACE_GETHBPREGS:
if (ptrace_get_breakpoints(child) < 0)
return -ESRCH;
ret = ptrace_gethbpregs(child, addr,
(unsigned long __user *)data);
ptrace_put_breakpoints(child);
break;
case PTRACE_SETHBPREGS:
if (ptrace_get_breakpoints(child) < 0)
return -ESRCH;
ret = ptrace_sethbpregs(child, addr,
(unsigned long __user *)data);
ptrace_put_breakpoints(child);
break;
#endif
static int ptrace_gethbpregs(struct task_struct *tsk, long num,
unsigned long __user *data)
{
u32 reg;
int idx, ret = 0;
struct perf_event *bp;
struct arch_hw_breakpoint_ctrl arch_ctrl;
if (num == 0) {
reg = ptrace_get_hbp_resource_info();
} else {
idx = ptrace_hbp_num_to_idx(num);
if (idx < 0 || idx >= ARM_MAX_HBP_SLOTS) {
ret = -EINVAL;
goto out;
}
bp = tsk->thread.debug.hbp[idx];
if (!bp) {
reg = 0;
goto put;
}
arch_ctrl = counter_arch_bp(bp)->ctrl;
/*
* Fix up the len because we may have adjusted it
* to compensate for an unaligned address.
*/
while (!(arch_ctrl.len & 0x1))
arch_ctrl.len >>= 1;
if (num & 0x1)
reg = bp->attr.bp_addr;
else
reg = encode_ctrl_reg(arch_ctrl);
}
put:
if (put_user(reg, data))
ret = -EFAULT;
out:
return ret;
}
顺着ptrace_get_hbp_resource_info一路看下去,其调用hw_breakpoint_slots,后者再调用get_num_brps,再后者再调用ARM_DBG_READ读取调试寄存器的值。
在ptrace_get_hbp_resurce_info中,可以看到其返回值由4个字节组合而成,最高字节是系统支持的硬件断点数(最大不超过16),次高字节是系统支持的数据断点数(watchpoint)(最大不超过16),次低字节是watchpoint支持的最大长度,最低字节是调试信息版本号:
static u32 ptrace_get_hbp_resource_info(void)
{
u8 num_brps, num_wrps, debug_arch, wp_len;
u32 reg = 0;
num_brps = hw_breakpoint_slots(TYPE_INST);
num_wrps = hw_breakpoint_slots(TYPE_DATA);
debug_arch = arch_get_debug_arch();
wp_len = arch_get_max_wp_len();
reg |= debug_arch;
reg <<= 8;
reg |= wp_len;
reg <<= 8;
reg |= num_wrps;
reg <<= 8;
reg |= num_brps;
return reg;
}
int hw_breakpoint_slots(int type)
{
if (!debug_arch_supported())
return 0;
/*
* We can be called early, so don't rely on
* our static variables being initialised.
*/
switch (type) {
case TYPE_INST:
return get_num_brps();
case TYPE_DATA:
return get_num_wrps();
default:
pr_warning("unknown slot type: %d\n", type);
return 0;
}
}
/* Determine number of usable BRPs available. */
static int get_num_brps(void)
{
int brps = get_num_brp_resources();
if (core_has_mismatch_brps())
brps -= get_num_reserved_brps();
return brps;
}
/* Determine number of BRP register available. */
static int get_num_brp_resources(void)
{
u32 didr;
ARM_DBG_READ(c0, 0, didr);
return ((didr >> 24) & 0xf) + 1;
}
/* Accessor macros for the debug registers. */
#define ARM_DBG_READ(M, OP2, VAL) do {\
asm volatile("mrc p14, 0, %0, c0," #M ", " #OP2 : "=r" (VAL));\
} while (0)
#define ARM_DBG_WRITE(M, OP2, VAL) do {\
asm volatile("mcr p14, 0, %0, c0," #M ", " #OP2 : : "r" (VAL));\
} while (0)
继续来看ptrace_sethbpregs的实现,它先校验输入信息的有效性,然后用decode_ctrl_reg将信息组合成要写入到DBG寄存器的值,最后调用modify_user_hw_breakpoint来提交设置动作。
static int ptrace_sethbpregs(struct task_struct *tsk, long num,
unsigned long __user *data)
{
int idx, gen_len, gen_type, implied_type, ret = 0;
u32 user_val;
struct perf_event *bp;
struct arch_hw_breakpoint_ctrl ctrl;
struct perf_event_attr attr;
if (num == 0)
goto out;
else if (num < 0)
implied_type = HW_BREAKPOINT_RW;
else
implied_type = HW_BREAKPOINT_X;
idx = ptrace_hbp_num_to_idx(num);
if (idx < 0 || idx >= ARM_MAX_HBP_SLOTS) {
ret = -EINVAL;
goto out;
}
if (get_user(user_val, data)) {
ret = -EFAULT;
goto out;
}
bp = tsk->thread.debug.hbp[idx];
if (!bp) {
bp = ptrace_hbp_create(tsk, implied_type);
if (IS_ERR(bp)) {
ret = PTR_ERR(bp);
goto out;
}
tsk->thread.debug.hbp[idx] = bp;
}
attr = bp->attr;
if (num & 0x1) {
/* Address */
attr.bp_addr = user_val;
} else {
/* Control */
decode_ctrl_reg(user_val, &ctrl);
ret = arch_bp_generic_fields(ctrl, &gen_len, &gen_type);
if (ret)
goto out;
if ((gen_type & implied_type) != gen_type) {
ret = -EINVAL;
goto out;
}
attr.bp_len = gen_len;
attr.bp_type = gen_type;
attr.disabled = !ctrl.enabled;
}
ret = modify_user_hw_breakpoint(bp, &attr);
out:
return ret;
}
int modify_user_hw_breakpoint(struct perf_event *bp, struct perf_event_attr *attr)
{
u64 old_addr = bp->attr.bp_addr;
u64 old_len = bp->attr.bp_len;
int old_type = bp->attr.bp_type;
int err = 0;
perf_event_disable(bp);
bp->attr.bp_addr = attr->bp_addr;
bp->attr.bp_type = attr->bp_type;
bp->attr.bp_len = attr->bp_len;
if (attr->disabled)
goto end;
err = validate_hw_breakpoint(bp);
if (!err)
perf_event_enable(bp);
if (err) {
bp->attr.bp_addr = old_addr;
bp->attr.bp_type = old_type;
bp->attr.bp_len = old_len;
if (!bp->attr.disabled)
perf_event_enable(bp);
return err;
}
end:
bp->attr.disabled = attr->disabled;
return 0;
}
为了看perf_event上面的断点怎么工作的,需要从3.1.10/kernel/events/hw_breakpoint.c中的init_hw_breakpoint函数看起:
int __init init_hw_breakpoint(void)
{
unsigned int **task_bp_pinned;
int cpu, err_cpu;
int i;
for (i = 0; i < TYPE_MAX; i++)
nr_slots[i] = hw_breakpoint_slots(i);
for_each_possible_cpu(cpu) {
}
constraints_initialized = 1;
perf_pmu_register(&perf_breakpoint, "breakpoint", PERF_TYPE_BREAKPOINT);
return register_die_notifier(&hw_breakpoint_exceptions_nb);
err_alloc:
}
这里注册了perf_breakpoint的操作,来看全局变量perf_breakpoint的定义:
static struct pmu perf_breakpoint = {
.task_ctx_nr = perf_sw_context, /* could eventually get its own */
.event_init = hw_breakpoint_event_init,
.add = hw_breakpoint_add,
.del = hw_breakpoint_del,
.start = hw_breakpoint_start,
.stop = hw_breakpoint_stop,
.read = hw_breakpoint_pmu_read,
};
static int hw_breakpoint_add(struct perf_event *bp, int flags)
{
if (!(flags & PERF_EF_START))
bp->hw.state = PERF_HES_STOPPED;
return arch_install_hw_breakpoint(bp);
}
/*
* Install a perf counter breakpoint.
*/
int arch_install_hw_breakpoint(struct perf_event *bp)
{
struct arch_hw_breakpoint *info = counter_arch_bp(bp);
struct perf_event **slot, **slots;
int i, max_slots, ctrl_base, val_base, ret = 0;
u32 addr, ctrl;
/* Ensure that we are in monitor mode and halting mode is disabled. */
ret = enable_monitor_mode();
if (ret)
goto out;
addr = info->address;
ctrl = encode_ctrl_reg(info->ctrl) | 0x1;
if (info->ctrl.type == ARM_BREAKPOINT_EXECUTE) {
/* Breakpoint */
ctrl_base = ARM_BASE_BCR;
val_base = ARM_BASE_BVR;
slots = (struct perf_event **)__get_cpu_var(bp_on_reg);
max_slots = core_num_brps;
if (info->step_ctrl.enabled) {
/* Override the breakpoint data with the step data. */
addr = info->trigger & ~0x3;
ctrl = encode_ctrl_reg(info->step_ctrl);
}
} else {
/* Watchpoint */
if (info->step_ctrl.enabled) {
/* Install into the reserved breakpoint region. */
ctrl_base = ARM_BASE_BCR + core_num_brps;
val_base = ARM_BASE_BVR + core_num_brps;
/* Override the watchpoint data with the step data. */
addr = info->trigger & ~0x3;
ctrl = encode_ctrl_reg(info->step_ctrl);
} else {
ctrl_base = ARM_BASE_WCR;
val_base = ARM_BASE_WVR;
}
slots = (struct perf_event **)__get_cpu_var(wp_on_reg);
max_slots = core_num_wrps;
}
for (i = 0; i < max_slots; ++i) {
slot = &slots[i];
if (!*slot) {
*slot = bp;
break;
}
}
if (WARN_ONCE(i == max_slots, "Can't find any breakpoint slot\n")) {
ret = -EBUSY;
goto out;
}
/* Setup the address register. */
write_wb_reg(val_base + i, addr);
/* Setup the control register. */
write_wb_reg(ctrl_base + i, ctrl);
out:
return ret;
}
arch_install_hw_breakpoint先对信息组合(encode_ctrl_reg),再调用write_wb_reg分别将断点地址信息和断点控制信息写入到DEBUG寄存器。
write_wb_reg是对CP14操作的一系列宏组成的,各宏的引用如下,最终是调用ARM_DBG_WRITE宏来写入值,即通过汇编操作协处理器CP14写入到DEBUG寄存器中。
static void write_wb_reg(int n, u32 val)
{
switch (n) {
GEN_WRITE_WB_REG_CASES(ARM_OP2_BVR, val);
GEN_WRITE_WB_REG_CASES(ARM_OP2_BCR, val);
GEN_WRITE_WB_REG_CASES(ARM_OP2_WVR, val);
GEN_WRITE_WB_REG_CASES(ARM_OP2_WCR, val);
default:
pr_warning("attempt to write to unknown breakpoint "
"register %d\n", n);
}
isb();
}
#define GEN_WRITE_WB_REG_CASES(OP2, VAL) \
WRITE_WB_REG_CASE(OP2, 0, VAL); \
WRITE_WB_REG_CASE(OP2, 1, VAL); \
WRITE_WB_REG_CASE(OP2, 2, VAL); \
WRITE_WB_REG_CASE(OP2, 3, VAL); \
WRITE_WB_REG_CASE(OP2, 4, VAL); \
WRITE_WB_REG_CASE(OP2, 5, VAL); \
WRITE_WB_REG_CASE(OP2, 6, VAL); \
WRITE_WB_REG_CASE(OP2, 7, VAL); \
WRITE_WB_REG_CASE(OP2, 8, VAL); \
WRITE_WB_REG_CASE(OP2, 9, VAL); \
WRITE_WB_REG_CASE(OP2, 10, VAL); \
WRITE_WB_REG_CASE(OP2, 11, VAL); \
WRITE_WB_REG_CASE(OP2, 12, VAL); \
WRITE_WB_REG_CASE(OP2, 13, VAL); \
WRITE_WB_REG_CASE(OP2, 14, VAL); \
WRITE_WB_REG_CASE(OP2, 15, VAL)
#define WRITE_WB_REG_CASE(OP2, M, VAL) \
case ((OP2 << 4) + M): \
ARM_DBG_WRITE(c ## M, OP2, VAL);\
break
#define ARM_DBG_WRITE(M, OP2, VAL) do {\
asm volatile("mcr p14, 0, %0, c0," #M ", " #OP2 : : "r" (VAL));\
} while (0)
写入到DEBUG寄存器后,所下的断点信息就生效了。
在x86中,通过操作DR寄存器下断点。
在mips中,也有专用的DEBUG寄存器。
未完,后面再来补充。