最近安卓項目中碰到一個踩記憶體導致死機的問題,通過分析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 (<span style="color:#ff0000;"><strong>ptrace (PTRACE_SETHBPREGS, pid, (PTRACE_TYPE_ARG3) ((i << 1) + 1), &bpts[i].address)</strong></span> < 0) perror_with_name (_("Unexpected error setting breakpoint")); if (bpts[i].control != 0) if (<span style="color:#ff0000;"><strong>ptrace (PTRACE_SETHBPREGS, pid, (PTRACE_TYPE_ARG3) ((i << 1) + 2), &bpts[i].control)</strong></span> < 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 (<span style="color:#ff0000;"><strong>ptrace (PTRACE_SETHBPREGS, pid, (PTRACE_TYPE_ARG3) -((i << 1) + 1), &wpts[i].address)</strong></span> < 0) perror_with_name (_("Unexpected error setting watchpoint")); if (wpts[i].control != 0) if (<span style="color:#ff0000;"><strong>ptrace (PTRACE_SETHBPREGS, pid, (PTRACE_TYPE_ARG3) -((i << 1) + 2), &wpts[i].control)</strong></span> < 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_BREAKPOINTcase 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
在ptrace_gethbpregs中,如果num為0,調用ptrace_get_hbp_resource_info來擷取調試資訊,如果num不為0,先調用ptrace_hbp_num_to_idx將num轉換為索引(按前文的+1,+2, +3,+4, ... 規則轉換,然後返回線程中所緩衝的調試配置資訊。
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;elsereg = 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;}
最關鍵的,來看ARM_DBG_READ的實現。ARM_DBG_READ和ARM_DBG_WRITE其實是一組宏,用來讀取和寫入調試寄存器,它們在ARM上就是一條彙編,通過mrc和mcr操作副處理器CP14來完成調試功能,其定義如下:
/* 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)
以上就看到了linux如何操作CP14來擷取調試資訊。
繼續來看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;elseimplied_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;}
modify_user_hw_breakpoint只是將動作通過perf_event_enable提交。
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,};
所以硬體斷點的添加,會進入hw_breakpoint_add函數,而hw_breakpoint_add函數只是簡單調用架構相關的函數arch_install_hw_breakpoint來實現,在ARM,該函數在3.1.10/arch/arm/kernel/hw_breakpoint.c中:
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寄存器。
未完,後面再來補充。