如何利用硬件watchpoint定位踩內存問題【轉】

轉自:http://blog.coderhuo.tech/2019/07/21/arm_hardware_breakpoint/

本文介紹如何使用ARM平臺的硬件watchpoint定位踩內存問題,特別是如何在運行過程中自動對特定內存區域添加watchpoint。

在踩內存問題中,最困難的就是找出元兇。常見的作法如下:

  1. 通過gdb打內存斷點(添加watchpoint), 看看誰非法訪問了該內存區域。本方法的侷限性在於:有些系統不支持gdb,或者被踩內存地址不固定,或者問題出現在啓動階段,來不及設置斷點。
  2. 通過MMU(Linux下可以使用mrotect)對特定內存區域進行保護。本方法的侷限性在於:MMU保護的最小單位是一個內存頁(一般爲4KB),有可能受害內存區域較小,無法用MMU進行保護。
  3. dump事發現場周邊的內存,通過關鍵字識別誰對這塊內存進行了非法寫入。比如受害內存區域中有0xAABB字樣,而只有某個模塊會產生0xAABB的數據,基於此就可以鎖定兇手。但是並非每個模塊的數據都是有特徵的,大部分情況下無法通過該方法找到兇手。

這時可以嘗試芯片自帶的硬件watchpoint功能, ARM平臺和x86/64一般均支持。其實gdb的watchpoint大多數情況下就是基於硬件中斷實現的(https://sourceware.org/gdb/wiki/Internals%20Watchpoints)。

下面基於ARM Cortex-A9介紹如何使用該功能,本文大量引用《ARM® Cortex®-A9 Technical Reference Manual》 、《ARM® Architecture Reference Manual ARMv7-A and ARMv7-R edition》hw_breakpoint.c中的內容。

一、首先確認是否支持硬件watchpoint

這個必須查對應芯片的技術手冊。從《ARM® Cortex®-A9 Technical Reference Manual》10.3.2節(Breakpoints and watchpoints)可以看到,本處理器支持6個breakpoints、4個watchpoints。

二、打開監控模式

本節的原理可以參考《ARM® Architecture Reference Manual ARMv7-A and ARMv7-R edition》 C11.11.20 DBGDSCR, Debug Status and Control Register 這一節。

本節所展示代碼均摘自或基於hw_breakpoint.c中相關代碼改寫。

要想使用硬件watchpoint,必須先打開監控模式。下面的代碼可以判斷當前是否已經打開監控模式:

/*
 * In order to access the breakpoint/watchpoint control registers,
 * we must be running in debug monitor mode.
 */
static int monitor_mode_enabled(void)
{
	u32 dscr;
	ARM_DBG_READ(c0, c1, 0, dscr);
	return !!(dscr & ARM_DSCR_MDBGEN);
}

其中寄存器讀寫操作的宏定義如下:

#define ARM_DSCR_HDBGEN		(1 << 14) /* DSCR halting bits. */
#define ARM_DSCR_MDBGEN		(1 << 15) /* DSCR monitor bits. */

/* Accessor macros for the debug registers. */
#define ARM_DBG_READ(N, M, OP2, VAL) do {\
	asm volatile("mrc p14, 0, %0, " #N "," #M ", " #OP2 : "=r" (VAL));\
} while (0)

#define ARM_DBG_WRITE(N, M, OP2, VAL) do {\
	asm volatile("mcr p14, 0, %0, " #N "," #M ", " #OP2 : : "r" (VAL));\
} while (0)

下面的代碼可以打開監控模式:

static int enable_monitor_mode(void)
{
	u32 dscr;
	ARM_DBG_READ(c0, c1, 0, dscr);

	/* If monitor mode is already enabled, just return. */
	if (dscr & ARM_DSCR_MDBGEN)
		goto out;

	/* Write to the corresponding DSCR. */
	/* 根據不同的平臺,調用不同的代碼打開監控模式 */
	switch (get_debug_arch()) {
	case ARM_DEBUG_ARCH_V6:
	case ARM_DEBUG_ARCH_V6_1:
		ARM_DBG_WRITE(c0, c1, 0, (dscr | ARM_DSCR_MDBGEN));
		break;
	case ARM_DEBUG_ARCH_V7_ECP14:
	case ARM_DEBUG_ARCH_V7_1:
	case ARM_DEBUG_ARCH_V8:
		ARM_DBG_WRITE(c0, c2, 2, (dscr | ARM_DSCR_MDBGEN));
		isb();
		break;
	default:
		return -ENODEV;
	}

	/* Check that the write made it through. */
	/* 檢查是否已經打開監控模式 */
	ARM_DBG_READ(c0, c1, 0, dscr);
	if (!(dscr & ARM_DSCR_MDBGEN)) {
		pr_warn_once("Failed to enable monitor mode on CPU %d.\n",
				smp_processor_id());
		return -EPERM;
	}

out:
	return 0;
}

三、設置watchpoint

本節的原理可以參考《ARM® Cortex®-A9 Technical Reference Manual》 10.5.3 (Watchpoint Value Registers)和 10.5.4(Watchpoint Control Registers) 這兩節。

本節所展示代碼均摘自或基於hw_breakpoint.c中相關代碼改寫。

硬件watchpoint功能,是由Watchpoint Value Register(WVR)和Watchpoint Control Register(WCR)兩個寄存器配對實現的,前者設置被監控地址(虛擬內存地址,而不是物理內存地址,這省去了轉換環節,極大的方便了調試),後者進行控制。

下面的代碼可以用來設置Watchpoint,它的作用是:如果有人在用戶態往addr開始的前兩個字節寫入內容,就會產生異常。

/*
 * i: watchpoint寄存器序號,對於ARM-A9, 可用寄存器爲c0~c3, i 取值範圍爲0~3
 * addr: 必須是虛擬內存地址,且必須是字對齊的(32位系統爲4字節對齊)
 * we must be running in debug monitor mode.
 */

int arm_install_hw_watchpoint(int i, u32 addr)
{
	u32 ctrl = 0x117;
	u32 check_value;

	/* Setup the address register. */
	write_wb_reg(ARM_BASE_WVR + i, addr);
	check_value = read_wb_reg(ARM_BASE_WVR + i);
	if (check_value != addr)
	{
		pr_warn("fail to set WVR[%d] addr:0x%x check_value:0x%x\n", i, addr, check_value);
		return -1;
	}

	/* Setup the control register. */
	write_wb_reg(ARM_BASE_WCR + i, ctrl);
	check_value = read_wb_reg(ARM_BASE_WCR + i);
	if (check_value != ctrl)
	{
		pr_warn("fail to set WCR[%d] ctrl:0x%x check_value:0x%x\n", i, ctrl, check_value);
		return -1;
	}

	return 0;
}

其中ctrl = 0x117(二進制‭0011 10 10 1‬)的解釋如下,主要參考《ARM® Cortex®-A9 Technical Reference Manual》 10.5.4(Watchpoint Control Registers) 這一節:

  1. 最低位的1表示開啓Watchpoint功能
  2. 再向上的10代表僅監控用戶模式下對該內存區域的操作
  3. 再向上的10代表僅監控往該內存區域的寫入操作
  4. Watchpoint監控的虛擬內存地址爲字對齊的(32位系統爲4字節對齊),每個Watchpoint最大監控長度爲4字節(32位系統)。但是如果僅僅想監控1個字節或者2個字節呢?最前面的四個比特0011就是用來做這個事情的。上面的0011代表僅監控addr開始的2個字節。如果只想監控最後一個字節,前4比特可以寫爲0001。

上面代碼中引用函數/宏定義如下:

#define isb() __asm__ __volatile__ ("" : : : "memory")

#define READ_WB_REG_CASE(OP2, M, VAL)			\
	case ((OP2 << 4) + M):				\
		ARM_DBG_READ(c0, c ## M, OP2, VAL);	\
		break

#define WRITE_WB_REG_CASE(OP2, M, VAL)			\
	case ((OP2 << 4) + M):				\
		ARM_DBG_WRITE(c0, c ## M, OP2, VAL);	\
		break

#define GEN_READ_WB_REG_CASES(OP2, VAL)		\
	READ_WB_REG_CASE(OP2, 0, VAL);		\
	READ_WB_REG_CASE(OP2, 1, VAL);		\
	READ_WB_REG_CASE(OP2, 2, VAL);		\
	READ_WB_REG_CASE(OP2, 3, VAL);		\
	READ_WB_REG_CASE(OP2, 4, VAL);		\
	READ_WB_REG_CASE(OP2, 5, VAL);		\
	READ_WB_REG_CASE(OP2, 6, VAL);		\
	READ_WB_REG_CASE(OP2, 7, VAL);		\
	READ_WB_REG_CASE(OP2, 8, VAL);		\
	READ_WB_REG_CASE(OP2, 9, VAL);		\
	READ_WB_REG_CASE(OP2, 10, VAL);		\
	READ_WB_REG_CASE(OP2, 11, VAL);		\
	READ_WB_REG_CASE(OP2, 12, VAL);		\
	READ_WB_REG_CASE(OP2, 13, VAL);		\
	READ_WB_REG_CASE(OP2, 14, VAL);		\
	READ_WB_REG_CASE(OP2, 15, VAL)

#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)

static u32 read_wb_reg(int n)
{
	u32 val = 0;

	switch (n) {
	GEN_READ_WB_REG_CASES(ARM_OP2_BVR, val);
	GEN_READ_WB_REG_CASES(ARM_OP2_BCR, val);
	GEN_READ_WB_REG_CASES(ARM_OP2_WVR, val);
	GEN_READ_WB_REG_CASES(ARM_OP2_WCR, val);
	default:
		pr_warn("attempt to read from unknown breakpoint register %d\n", n);
	}

	return val;
}

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_warn("attempt to write to unknown breakpoint register %d\n", n);
	}
	isb();
}

注意:cache操作不會產生watchpoint事件(Cache maintenance operations do not generate watchpoint events)

有了上面的arm_install_hw_watchpoint函數,我們就可以根據需要在程序運行過程中動態的對特定地址添加監控,看看誰踩了內存。

四、參考資料

  1. 《ARM® Cortex®-A9 Technical Reference Manual》
  2. 《ARM® Architecture Reference Manual ARMv7-A and ARMv7-R edition》
  3. https://sourceware.org/gdb/wiki/Internals%20Watchpoints
  4. hw_breakpoint.c
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章