How to debug memory - XradioTech/XR871-OLD GitHub Wiki

SDK内存调试方法


1 概述

嵌入式开发中,小系统的内存空间有限,各种内存问题的排查十分耗时。为了提高排查内存问题的效率,SDK提供了若干手段可用于堆(heap)使用的统计、内存泄漏检查、内存越界检查等。

2 堆统计

2.1.1 粗略堆统计

SDK的堆管理采用了标准C库中的malloc()、free()等系列函数,可对free()后相邻的内存空间进行合并,减少内存碎片化。但是标准C库无法对堆使用量进行精确统计,只能记录其使用最大值。

获取标准C库中堆信息(堆起始地址、堆结束地址、堆分配的最大地址)的API如下:

void heap_get_space(uint8_t **start, uint8_t **end, uint8_t **current);

SDK提供了获取以上信息的命令,使用如下所示:

$ heap space
<ACK> 200 heap total 122784, use 9472, free 113312, [0x61c60, 0x64160, 0x7fc00)

2.1.1 精确堆统计

如果要对堆使用进行精确统计,需要执行以下操作:

  • 在“config.mk”中设置:__CONFIG_MALLOC_TRACE ?= y
  • 重新编译“src\libc\wrap_malloc.c

完成以上操作后,调用以下API可以打印堆使用的详细信息:

/*
 * @brief 打印堆使用情况的简要信息和详细信息(每个内存块的起始地址和大小),检查每个内存块
 *         是否存在内存越界。
 * @param[in] verbose 是否打印堆使用的详细信息
 * @return 当前堆的使用总量
 */
uint32_t wrap_malloc_heap_info(int verbose);

SDK提供了调用**wrap_malloc_heap_info()**打印堆信息的命令,使用如下所示:

$ heap info 0
<<< heap info >>>
g_mem_sum       5841 (5 KB)
g_mem_sum_max   6961 (6 KB)
g_mem_entry_cnt 16, max 18
<ACK> 200 heap total 122784 (119 KB), use 5841 (5 KB), free 116943 (114 KB)

$ heap info 1
<<< heap info >>>
g_mem_sum       5841 (5 KB)
g_mem_sum_max   6961 (6 KB)
g_mem_entry_cnt 16, max 18
001. 000, 0x61c68, 88
002. 001, 0x61cc8, 88
003. 004, 0x621a8, 512
004. 005, 0x623b0, 96
005. 006, 0x62420, 249
006. 007, 0x62528, 2048
007. 008, 0x62d30, 96
008. 009, 0x63358, 88
009. 010, 0x633c0, 36
010. 011, 0x633f0, 152
011. 012, 0x63490, 36
012. 013, 0x634c0, 88
013. 014, 0x63528, 32
014. 015, 0x638f8, 88
015. 016, 0x63958, 2048
016. 017, 0x63888, 96
<ACK> 200 heap total 122784 (119 KB), use 5841 (5 KB), free 116943 (114 KB)

精确堆统计的原理是采用静态数组跟踪堆中内存块的分配和释放:

  • 每个数组元素占用8个字节,默认的数组大小可记录1024个内存块信息(总共占用8192字节内存)。
  • 数组的大小由“src\libc\wrap_malloc.c”中的宏“HEAP_MEM_MAX_CNT”指定。
  • HEAP_MEM_MAX_CNT”的值需要根据系统中分配使用的内存块个数来确定。如果“HEAP_MEM_MAX_CNT”过小,则可能发生某些内存块的分配无法跟踪记录,导致统计信息有误。

3 内存泄漏

系统可以通过调用malloc()从堆中分配一块内存块,使用结束后需要调用free()进行释放。如果忘记调用free()对堆内存块进行释放,则会出现内存泄漏。如果这种情况频繁发生,则会导致系统可用堆空间越来越少。

通过在一段代码的执行前后分别调用**wrap_malloc_heap_info(1)**查看堆内存块的详细信息,再对比前后两次的内存块列表,即可检查出是否存在内存泄漏的情况。

4 内存多次释放

如果一块malloc()分配的堆内存,调用free()进行了多次释放,将会损坏堆空间。这种损坏导致的异常现象不会立刻表现出来,而会延迟到后面的某一次堆使用时才出现,所以,这种错误的代码逻辑会在系统中埋下不可预知的严重隐患,必须杜绝。

SDK在开启精确堆统计功能后,可以检测出内存多次释放的问题。

例如,错误代码如下:

void heap_double_free(void)
{
	uint8_t *buf = malloc(16);
	printf("buf %p\n", buf);
	free(buf);
	free(buf);
}

运行后的错误信息如下:

buf 0x63ab8
[heap ERR] wrap_malloc_delete_entry():192, heap mem entry (0x63ab8) missed

5 内存越界

如果向堆内存块中写入超过其长度的数据时,就发生内存写越界的情况,导致堆内存链表错误或者其他内存块被改写。这种损坏导致的异常现象不会立刻表现出来,而会延迟到后面的某一次内存使用时才出现,所以,这种错误的代码逻辑会在系统中埋下不可预知的严重隐患,必须杜绝。

SDK在开启精确堆统计功能后,可以对堆内存块的写越界进行检查。检查原理如下:

  • 从堆中分配内存的时候,多分配4个字节。
  • 在这多分配的4个字节(处于内存块的末尾)中填入4个魔数:0x4a, 0x5b, 0x6c, 0x7f
  • 调用free()释放内存块时,检查内存块末尾的4字节魔数是否改变;如果改变了,则表示发生了内存写越界,并打印出错信息。
  • 调用wrap_malloc_heap_info()时,将会检查所有堆内存块末尾的4字节魔数是否改变;如果改变了,将会打印出错信息。

例如,错误代码如下:

void heap_overflow(void)
{
	char *buf = malloc(8);
	strcpy(buf, "12345678");
	wrap_malloc_heap_info(0);
	free(buf);
}

运行后的错误信息如下:

<<< heap info >>>
g_mem_sum       6969 (6 KB)
g_mem_sum_max   6969 (6 KB)
g_mem_entry_cnt 19, max 19
[heap ERR] wrap_malloc_heap_info():131, mem (0x63ab8) corrupt
[heap ERR] wrap_malloc_delete_entry():179, mem f (0x63ab8, 8) corrupt

6 内存跟踪

如果系统中存在内存泄漏、内存越界等情况,通过以上手段可以发现。但是,如果在系统比较复杂的情况下,想要进一步定位哪段代码出现了问题,出现错误的内存块属于哪个模块等,也不是一件容易的事情。SDK提供了一种内存跟踪手段,可以辅助定位类似问题。默认情况下,该手段是关闭的,需要执行以下操作进行开启:

  • 在“src\libc\wrap_malloc.c”中设置宏“HEAP_MEM_DBG_ON”为“1”。
  • 定义内存块的跟踪条件。例如要打印大小小于16字节的堆内存分配、释放的情况,定义如下:
#define HEAP_MEM_DBG_ON 		1
#define HEAP_MEM_DBG_MIN_SIZE	16
#define HEAP_MEM_IS_TRACED(size)	(size < HEAP_MEM_DBG_MIN_SIZE)

开启内存跟踪功能后,满足跟踪条件的内存块在分配、释放时,都会有相关的打印输出。

例如,演示代码如下:

void heap_trace(void)
{
	printf("heap trace begin\n");
	uint8_t *buf = malloc(8);
	printf("buf %p\n", buf);
	free(buf);
	printf("heap trace end\n");
}

代码运行后的打印信息如下:

heap trace begin
[heap] m (0x63b58, 8)
buf 0x63b58
[heap] f (0x63b58, 8)
heap trace end

内存跟踪的打印信息结合系统运行过程中的其他打印信息,可粗略定位内存的分配和释放是在哪些上下文代码中执行的,据此再进行进一步分析和定位。

⚠️ **GitHub.com Fallback** ⚠️