内存陷阱,一种内存调试方法
WangGaojie Lv4

需求场景

在一个多任务系统中,当一个全局可访问的数据出现了异常,如何排查问题呢?由于在多任务环境中,各个任务并行运行,一般的单步跟踪调试很难定位问题。逐个屏蔽代码又可能会导致问题无法复现。使用内存断点是比较好的方法,但是大多数集成开发环境对该功能的支持不完美,且使用场景有限。

下面引入一个问题代码场景,分析该问题的排查方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
uint32_t *p;
void task_A(void){
p = (uint32_t *)0x2000;

enable_isr_a();

while(1){
printf("freq :%u\n", *p);
*p = 0;
delay(1000);
}
}

void isr_a(void){
*p = *p + 1;
}

void task_B(void){
// ...
uint8_t *a = 0x2000;
*a = 100;
}

在这段程序中任务A分配了一段内存空间,并打开了一个外部中断,任务A统计中断发生的次数,每秒输出一次数据并清零。如果此时存在一个任务B,由于其它故障导致变量被分配到了同一段内存上,显然任务A统计次数的变量就会出现数据异常的现象。

由于地址0x2000会被正常的任务A和中断A访问,同时被异常的任务B访问。集成开发环境中的内存断点无法正常区分正常的访问和非法的访问,进而导致无法准确的跟踪到问题代码。

适时启用内存断点

如果能够在代码中实时的启用和关闭内存断点,看这个问题就能解决了。
大致看起来是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
uint32_t *p;
void task_A(void){
p = (uint32_t *)0x2000;

enable_isr_a();

while(1){
disable_mem_break(0x2000);
printf("freq :%u\n", *p);
*p = 0;
enable_mem_break(0x2000);
delay(1000);
}
}

void isr_a(void){
disable_mem_break(0x2000);
*p = *p + 1;
enable_mem_break(0x2000);
}

void task_B(void){
// ...
uint8_t *a = 0x2000;
*a = 100;
}

当正常访问数据时就先把内存断点关闭,当数据正常访问完成后再开,这样线程B非法访问数据时就会被内存断点捕获。这样在代码中适时添加、删除断点的行为是可行的。

内存断点的原理

ARM Cortex-M3以上的内核都具有CoreDebug和DWT内核外设模块,它们的一个功能就是实现内存断点机制,通过在DWT的监视表上记录需要监视的内存地址,监视的方式(读、写)等参数,当满足条件时就会触发DebugMon_Handler异常中断。这个过程显然是代码可控制的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct
{
__IOM uint32_t CTRL;
__IOM uint32_t CYCCNT;
__IOM uint32_t CPICNT;
__IOM uint32_t EXCCNT;
__IOM uint32_t SLEEPCNT;
__IOM uint32_t LSUCNT;
__IOM uint32_t FOLDCNT;
__IM uint32_t PCSR;
__IOM struct{
uint32_t COMP;
uint32_t MASK;
uint32_t FUNCTION;
uint32_t RESERVED;
} W[4];
} DWT_Type;

不同的处理器内核所具有的内存断点数量是不一样的,一般CM3、CM4、CM7只有4个,更高级的内核数量会多一些。同时监视4个地址下的内存能够满足很多分析场景。
打开内存监视功能需要写入COMP、MASK、FUNCTION三个寄存器,其中COMP寄存器写入待监视的地址,FUNCTION表示监视的方式(5:read 6:write 7:read&write 0表示关闭监视),MASK表示监视的数据位宽(0:8位 1:16位 2:32位)。

此外,还需要打开DebugMon_Handler异常中断,需要向CoreDebug模块的DEMCR寄存器第16位和24位写入1,用来打开DebugMon异常。

代码

mem_trap.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#ifndef _CPU_MEM_TRAP_H
#define _CPU_MEM_TRAP_H

typedef enum {
MEM_TRAP_DISABLE = 0,
MEM_READ_ONLY = 5,
MEM_WRITE_ONLY = 6,
MEM_READ_WRITE = 7
} mem_trap_mode_t;

typedef enum {
MEM_8BIT,
MEM_16BIT,
MEM_32BIT
} mem_width_t;

void mem0_watchpoint(void *addr, mem_trap_mode_t mode, mem_width_t width);
void mem0_watchpoint_reset(void);

void mem1_watchpoint(void *addr, mem_trap_mode_t mode, mem_width_t width);
void mem1_watchpoint_reset(void);

void mem2_watchpoint(void *addr, mem_trap_mode_t mode, mem_width_t width);
void mem2_watchpoint_reset(void);

void mem3_watchpoint(void *addr, mem_trap_mode_t mode, mem_width_t width);
void mem3_watchpoint_reset(void);

void mem_watchpoint_allreset(void);

#endif

mem_trap.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include "mem_trap.h"
#include <stdio.h>

#if defined(DWT) && defined(CoreDebug)

/* Symbol conflict */
#ifdef COMP1
#undef COMP1
#endif
#ifdef COMP2
#undef COMP2
#endif

#define DEF_MEM_WATCHPOINT(i) \
void mem##i##_watchpoint(void *addr, mem_trap_mode_t mode, mem_width_t width){\
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk |\
CoreDebug_DEMCR_MON_EN_Msk;\
DWT->FUNCTION##i = MEM_TRAP_DISABLE;\
DWT->COMP##i = (uint32_t)addr;\
DWT->MASK##i = (uint32_t)width;\
DWT->FUNCTION##i = (uint32_t)mode;\
}\
void mem##i##_watchpoint_reset(void){\
DWT->FUNCTION##i = MEM_TRAP_DISABLE;\
}

void mem_watchpoint_allreset(void){
DWT->FUNCTION0 = MEM_TRAP_DISABLE;
DWT->FUNCTION1 = MEM_TRAP_DISABLE;
DWT->FUNCTION2 = MEM_TRAP_DISABLE;
DWT->FUNCTION3 = MEM_TRAP_DISABLE;
}

DEF_MEM_WATCHPOINT(0)
DEF_MEM_WATCHPOINT(1)
DEF_MEM_WATCHPOINT(2)
DEF_MEM_WATCHPOINT(3)

__WEAK void mem_trap_log(int idx, uint32_t pc, uint32_t addr, const char *event){
printf("[%d]Trigger memory trap near by %#08x, %s at %#08x\r\n", \
idx, (unsigned int)pc, event, (unsigned int)addr);
}

void watchpoint_event_handle(uint32_t *args){
#define EVENT_PC (args[6])
const char* mem_event[4] = {"READ", "WRITE", "READ_WRITE", "ERROR"};

if((DWT->FUNCTION0 & DWT_FUNCTION_MATCHED_Msk) != 0){
mem_trap_log(0, EVENT_PC, DWT->COMP0, mem_event[(DWT->FUNCTION0 - 5) & 0x3]);
}

if((DWT->FUNCTION1 & DWT_FUNCTION_MATCHED_Msk) != 0){
mem_trap_log(1, EVENT_PC, DWT->COMP1, mem_event[(DWT->FUNCTION1 - 5) & 0x3]);
}

if((DWT->FUNCTION2 & DWT_FUNCTION_MATCHED_Msk) != 0){
mem_trap_log(2, EVENT_PC, DWT->COMP2, mem_event[(DWT->FUNCTION2 - 5) & 0x3]);
}

if((DWT->FUNCTION3 & DWT_FUNCTION_MATCHED_Msk) != 0){
mem_trap_log(3, EVENT_PC, DWT->COMP3, mem_event[(DWT->FUNCTION3 - 5) & 0x3]);
}
}

#if defined(__CC_ARM)
__asm void DebugMon_Handler(void){
extern watchpoint_event_handle

/* *INDENT-OFF* */
PRESERVE8

tst lr, #4
ite eq
mrseq r0, msp
mrsne r0, psp
push {lr}
bl watchpoint_event_handle
pop {pc}
}
#elif defined(__GNUC__)
void DebugMon_Handler(void){
__asm volatile
(
"tst lr, #4 \n"
"ite eq \n"
"mrseq r0, msp \n"
"mrsne r0, psp \n"
"push {lr} \n"
"bl watchpoint_event_handle \n"
"pop {pc} \n"
);
}
#endif

#endif

注:这里引用了CMSIS的内容,可以需要inlcude相关的文件。

DebugMon异常处理函数使用汇编实现的原因是为了在代码中自动定位到触发内存监视的PC指针位置,也就是代码位置。
代码的用法也比较简单,当需要保护某块代码中的数据不被异常访问,我们就使用mem0_watchpoint_reset()和mem0_watchpoint()将代码包起来。

以最开始的那段代码为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
uint32_t *p;
void task_A(void){
p = (uint32_t *)0x2000;

enable_isr_a();

while(1){
mem0_watchpoint_reset();
printf("freq :%u\n", *p);
*p = 0;
mem0_watchpoint(p, MEM_READ_WRITE, MEM_32BIT); // 开启保护
delay(1000);
}
}

void isr_a(void){
mem0_watchpoint_reset();
*p = *p + 1;
mem0_watchpoint(p, MEM_READ_WRITE, MEM_32BIT);
}

void task_B(void){
// ...
uint8_t *a = 0x2000;
*a = 100;
}

当任务C运行时由于内存监视器的存在且打开了对适当内存的保护,访问0x2000地址时就会触发DebugMon异常,该异常函数能够自动分析出任务B中修改数据时的PC指针位置。

总结

在代码中使用这种内存跟踪机制在解决某些内存问题时是很有效的,它对正常的数据访问没有任何影响,但是在正常的数据访问之外,我们在那个数据点上挖了一个陷阱,任何尝试访问数据的代码都会掉入到这个陷阱中并被我们捕获,所有我把这种内存调试方法成为内存陷阱。

这里对DWT的机制以及寄存器只是一个简单的描述,如果需要了解其中的细节请查阅《ARMv7-M Architecture Reference Manual》,里面有非常详细的描述。