背景

       FreeRTOS系统在实际使用过程中,应用上经常遇到内存泄露,踩内存等相关内存问题。但是RTOS系统没有完善的检测工具。大部分可用的检测工具要不需要交叉编译工具链支持,要不只支持Linux等操作系统。

       因此对内存问题没有较好的第三方排查工具进行协助定位。

内存分析

       FreeRTOS系统默认提供内存检测相关接口,对于系统的内存初步分析可以使用。但是对于更细的内存泄露或者踩内存问题,默认接口难以支持。

内存管理函数

       内存管理实现方式有5种,具体需要根据RTOS确认使用哪种,对于5种内存管理的方式heap1到heap5,具体区别不再描述,具体查询https://freertos.blog.csdn.net/article/details/51606068?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~default-1.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~default-1.no_search_link

       系统默认自带的内存相关接口如下,对于接口4和接口5可以在适当情况下,进行查询相关内存动态信息,已确认是否存在内存泄露问题。理论上,一个稳定的系统,查询到的内存信息在一个正常范围内波动,不会出现长时间运行后可用内存减少的情况。如果出现内存逐步减少的情况,那么大概率系统存在内存泄露的问题。

内存分配失败

       步骤1:

       系统开启下面的宏定义,即打印动态内存分配失败回调函数功能。如果调用上述pvPortMalloc接口分配100字节内存,系统无法分配足够的内存,那么系统将通过该回调函数通知用户。

步骤2:

内存分配失败函数默认是弱函数,开发人员可重定义和增加相关内容。

堆栈溢出检测

       步骤1:

       栈溢出的检测方法有2种,一般情况下推荐使用第二种,即设置下面宏定义设置2。检测方法1比较简单,检测当前栈地址是否达到最大值。检测方法2在1的基础上,在初始化时用“a5a5a5a5”二进制数据进行填充,任务切换时检测最后几个字节是否还是默认值,从而确认是否栈溢出。

       步骤2:

       栈溢出的回调函数实现,默认栈溢出后会进入下面的回调函数。回调函数默认是弱函数,开发人员可以在其他位置重新定义和实现相关内容。一般情况下,堆栈溢出后至少需要打印溢出任务名(Task Name),调试设备支持coredump信息的话,建议打印coredump信息。

       一般情况下,任务在初始化时,分配了合理的内存空间后不会出现栈溢出的情况。如果出现异常的栈内存,大部分情况下,可能是由于栈空间被踩导致。

内存泄露

       在默认系统函数”pvPortMalloc”和“vPortFree”中有对应的内存调试回调函数,但是默认情况下,函数都是空函数,没有具体的实现。

”pvPortMalloc”函数中对应的回调函数如下,返回分配内存的地址和分配长度。

” vPortFree”函数中对应的回调函数如下,返回释放内存的地址和释放长度。

      

       在排查内存泄露的情况时,上述的回调函数并不是很好,开发人员只能知道分配内存地址和分配长度。内存泄露最主要还是需要知道谁分配了地址,分配了多少,以及是否释放情况。基于上诉的需求,对内存分配回调函数进行重新定义,每次分配内存支持分配地址的输入。

       “trace_malloc”函数的核心思想和实现方式,每次分配内存记录对应的分配人员地址,分配内存地址,分配长度,同时输出相关信息。

       “trace_free”函数采用相同的方式,查询对应待释放内存地址,释放对应内存,打印相关信息。

       同时,对于内存情况支持实时查询和分析,将当前已申请内存信息进行收集和整理,并支持输出,输出信息包括申请人员地址,申请总长度(申请并且没有释放),申请次数(申请并且没有释放)。

       上述代码分配调试信息可以直接增加到”pvPortMalloc”函数和” vPortFree”函数中替换对应的“traceMALLOC”函数和“traceFREE”函数。如果在SDK层对底层malloc和free函数有封装,那么推荐可以在应用层增加该内存调试信息,除非需要排查SDK本身的内存泄露问题。

       应用层封装的malloc函数和free函数中,增加内存调试接口,排查应用层的内存泄露问题。“__builtin_return_address(0)”是GCC编译器提供的内置函数,用于获取当前函数调用栈中的指定帧(frame)的返回地址。参数值0表示当前帧,即当前函数的返回地址;参数值1表示上一帧,即当前函数的调用者的返回地址,依此类推(博流702L平台不支持)。

“__builtin_frame_address(0)”表示当前帧地址,该地址减去4字节就是函数返回地址(RISC V平台),即“__builtin_return_address(0)”地址。

        在实际调试过程中,上述的回溯地址依然存在一些问题。因为返回地址只能打印调用接口的上一级,对于底层问题定位比如“xQueueGenericCreate”函数中申请内存使用“pvPortMalloc”函数,实际输出返回地址是“xQueueGenericCreate”函数地址,但是该函数可能在很多地方进行使用,依然不能确认哪一个操作导致出现问题。

[23:16-58-03:917]19, caller:0x23004732, totalSize:1664, mallocTimes:11

对此,需要对该地址进行backtrace回溯相关调用关系,一般来说芯片可以支持backtrace功能并且可通过配置进行打开或者关闭。例如博流产品可通过”CONFIG_ENABLE_FP=1”这个编译配置打开backtrace功能。backTrace规则和平台有关,RISC-V平台下规则如下。

  1. fp(frame point)地址减去4字节(32位系统,64位系统减去8字节,偏移1个地址长度),得到函数的返回地址return address;
  2. fp(frame point)地址减去8字节,偏移2个地址长度,得到上一层函数的fp地址;
  3. 上一层函数的fp规则相同,按照1,2步骤进行依次回溯即可,理论上可以回溯所有的调用函数地址;

        具体实现如下,在backtrace功能打开的情况下,进行3层函数的回调地址输出,反之只输出当前函数返回地址。

案例说明

       在打开backtrace的情况下,内存状态统计如下。比如idx=48的输出是一个异常的malloc信息,那么可以根据打印的地址进行回溯对应的调用关系如下。

bt_conn_set_state0x23043bac)->k_fifo_init(0x2303f102)->xQueueGenericCreate(0x230049d6

       通过调用关系可以确认该函数是哪一层调用的,对应反馈哪一层进行排查即可。对于SDK上出现的问题,malloc内存可能出现在产品开发,SDK开发,原厂SDK(包括host和controller),上面的内存很显然是原厂SDK中的Host层进行的malloc。

       从目前大部分函数实现看,backtrace进行3级回溯基本可以找到对应问题调用点,如果对于调用层级不能满足的情况,可以回到4级甚至5级,但是这种情况需要考虑芯片内存的情况。

踩内存

踩内存主要由以下几种情况导致

  1. 指针或内存未被初始化:在使用指针或内存之前,如果没有进行正确的初始化,就可能导致读写不属于自己的内存区域,从而引发踩内存问题
  2. 内存分配未成功却使用:当内存分配函数(如malloc)返回NULL时,表示内存分配失败。如果此时仍然尝试使用该内存,就会引发踩内存错误
  3. 操作越过了内存的边界:例如,在数组操作中,如果索引超出了数组的范围,就可能访问到不属于该数组的内存区域,导致踩内存
  4. 释放了内存却被继续使用:当内存被释放后,其指针仍然被保留并可能被再次使用,这会导致对已经释放的内存进行读写操作,从而引发踩内存问题
  5. 内存分配成功但初始化不当:即使内存分配成功,如果初始化不当(如初始化为无效的值或未完全初始化),也可能导致在后续操作中误用该内存,进而引发踩内存
  6. 类型强制转换错误:在不匹配的类型之间进行强制转换,也可能导致访问错误的内存区域,从而引发踩内存问题

踩内存问题往往难以调试,因为它不一定立即导致程序崩溃,而可能在之后的某个时间点才出现崩溃或其他异常行为。因此,在编写和调试程序时,需要特别注意内存的使用和管理,以避免踩内存问题的发生。内存申请后,应该避免多个地方释放的情况,同时使用该内存时需要判断内存的有效性。对于线程中内存充足且内存需要随时申请的情况,建议使用静态内存,避免反复申请和释放。对于裸机开发情况,底层驱动中申请的无线缓存等建议使用静态申请或者初始化后申请,反初始化进行释放内存。

RTOS上踩内存大部分情况下,不能实时监测到,大部分情况下只有运行到被踩内存才能出现异常甚至崩溃(参考堆栈溢出检测)。在踩内存或内存越界情况下,使能RTOS内存检测功能,参考上文堆栈溢出检测。

首先,通过coredumpbacktrace信息基本可以确认内存溢出或者异常点是否是固定位置。判断异常点内存前后地址是否正常,如果Task溢出破坏可能是由于Task申请栈地址不够导致,如果异常点前后地址都是正常数据,那么大概率是由于异常访问导致。

对于每次异常崩溃位置相同情况,比较常规的做法就是缩小范围,确认哪个Task或者线程导致崩溃位置的参数修改,在Task进入和退出时打印输出这个异常位置值或者地址。当异常值出现时,可以确认这个异常在哪个Task中被修改,由此逐步缩小范围到某个函数或者操作。对于FreeRTOS系统中,Task切换函数中有预埋的调试函数,但是默认情况下内容为空。

其中“traceTask_SWITCHED_OUT()”函数表示当前Task切换出去了,可以在这个函数中增加调试信息,建议打印TaskIDname,以及异常点值或地址。“traceTask_SWITCHED_IN()”函数表达当前新的Task切换进去,可以增加同样的调试信息。

下面结构体中Task基本信息,不同RTOS存在差异,根据系统查找即可。其中”pcTaskName”表示Task创建时候设置的name,可以通过该字段判断不同的Task任务,当前也可以通过“uxTCBNumber”来确认不同的Task,这个值在Task创建时进行分配,每个Task创建后不会改变。

上述方式基本可以找到问题出现对应的Task,但是定位到具体的位置还是需要进一步排查。建议使用coredump信息分析和代码正面分析结合进行排查和确认。

总结

       本文从FreeRTOS自带的相关内存管理的接口和手段进行说明,引申到内存泄露和踩内存问题分析。

       内存泄露问题进行了详细的分析和描述,使用说明以及附上详细的代码,在实际项目调试中可操作性很强。但是对于踩内存问题,主要是描述原理和定位方法为主,不同踩内存方式没有很好的固定的排查方案。后期根据实际使用情况,可补充相关的排查案例,供读者参考排查思路。

附录(最新参考具体代码文件)

       traceMalloc示例代码如下。

#ifdef CONFIG_MEM_DEBUG

typedef struct
{
    void *ptr;
    size_t size;
    void *caller;  // actually the return address of malloc/free, used to trace where malloc/free is called
}malloc_entry_t;

typedef struct
{
    int table_full;
    uint32_t entry_num;
    uint32_t entry_num_max;
}malloc_table_info_t;

typedef struct 
{
    void* caller;
    uint32_t totalSize;
    uint32_t mallocTimes;
}mem_stats_t;

#define SYS_TRACE_MEM_ENTRY_NUM 120
#define SYS_TRACE_MEM_STATS_ENTRY_NUM  40//SYS_TRACE_MEM_ENTRY_NUM

malloc_entry_t malloc_entry[SYS_TRACE_MEM_ENTRY_NUM];
malloc_table_info_t malloc_table_info;
mem_stats_t mem_stats[SYS_TRACE_MEM_STATS_ENTRY_NUM];

uint32_t mallocCnt = 0;
uint32_t freeCnt = 0;

void trace_malloc(void *ptr, size_t size, void *caller)
{
    int i;

    if(ptr == NULL)
	{
        return;
    }

    if(!malloc_table_info.table_full)
	{
        for(i = 0; i < SYS_TRACE_MEM_ENTRY_NUM; i++)
		{
            if(malloc_entry[i].ptr == NULL)
			{
                malloc_entry[i].ptr = ptr;
                malloc_entry[i].size = size;
                malloc_entry[i].caller = caller;
				
                mallocCnt++;
				if(caller)
				{				
					printf("trace_malloc, caller=%08lx, ptr=%08lx, mallocCnt=%lu\r\n", (uint32_t)caller, (uint32_t)malloc_entry[i].ptr, mallocCnt);
				}
				else
				{	
					printf("trace_malloc, caller=NULL, ptr=%08lx, mallocCnt=%lu\r\n", (uint32_t)malloc_entry[i].ptr, mallocCnt);
				}
                break;
            }
        }
		
        if(i == SYS_TRACE_MEM_ENTRY_NUM)
		{
            malloc_table_info.table_full = 1;
        }
    }

    malloc_table_info.entry_num++;
    if(malloc_table_info.entry_num > malloc_table_info.entry_num_max)
	{
        malloc_table_info.entry_num_max = malloc_table_info.entry_num;
    }
}


void trace_free(void *ptr, void *caller)
{
    int i;

    if(ptr == NULL)
	{
        return;
    }

    if(!malloc_table_info.table_full)
	{
        for(i = 0; i < SYS_TRACE_MEM_ENTRY_NUM; i++)
		{
            if(malloc_entry[i].ptr == ptr)
			{
                freeCnt++;
				if(caller)
				{				
					printf("trace_free, caller=%08lx, ptr=%08lx, freeCnt=%lu\r\n", (uint32_t)caller, (uint32_t)malloc_entry[i].ptr, freeCnt);
				}
				else
				{	
					printf("trace_free, caller=NULL, ptr=%08lx, freeCnt=%lu\r\n", (uint32_t)malloc_entry[i].ptr, freeCnt);
				}
                malloc_entry[i].ptr = NULL;
				malloc_table_info.table_full = 0;   //add by dozen
                break;
            }
        }

		//not find malloc mem
		if(i == SYS_TRACE_MEM_ENTRY_NUM)
		{
			printf("trace_free error, caller=%08lx, ptr=%08lx, freeCnt=%lu\r\n", (uint32_t)caller, (uint32_t)ptr, freeCnt);
        }
    }

    malloc_table_info.entry_num--;
}

void Ez_MemTraceStats(void)
{
	u8 i,j;
	
    for(i = 0; i < SYS_TRACE_MEM_STATS_ENTRY_NUM; i++)
    {
        mem_stats[i].caller = NULL;
        mem_stats[i].totalSize = 0;
        mem_stats[i].mallocTimes = 0;
    }
    
    for(i = 0; i < SYS_TRACE_MEM_ENTRY_NUM; i++)
    {
        u8 fExist = 0;
        u16 firstEmpty = 0xFFFF;
		
        if(malloc_entry[i].ptr == NULL)
        {
            continue;
        }

        for(j = 0; j < SYS_TRACE_MEM_STATS_ENTRY_NUM; j++)
        {
            if(malloc_entry[i].caller == mem_stats[j].caller)
            {
                fExist = 1;
                mem_stats[j].mallocTimes++;
                mem_stats[j].totalSize += malloc_entry[i].size;
                break;
            }

            if(firstEmpty==0xFFFF && mem_stats[j].caller == NULL)
            {
                firstEmpty = j;
            }
        }

        if(!fExist && firstEmpty != 0xFFFF)
        {
            mem_stats[firstEmpty].caller = malloc_entry[i].caller;
            mem_stats[firstEmpty].totalSize = malloc_entry[i].size;
            mem_stats[firstEmpty].mallocTimes = 1;
        }
    }
    
    for(i = 0; i < SYS_TRACE_MEM_STATS_ENTRY_NUM; i++)
    {
        if(mem_stats[i].caller)
        {
            printf("%d, caller:0x%08lx, totalSize:%lu, mallocTimes:%lu\r\n", i, (uint32_t)mem_stats[i].caller, mem_stats[i].totalSize, mem_stats[i].mallocTimes);
        }
    }
    printf("Current left size is %d bytes,entry_num_max:%ld\r\n", xPortGetFreeHeapSize(), malloc_table_info.entry_num_max);
}


#endif

void *Ez_Malloc(size_t size)
{
	#ifdef CONFIG_MEM_DEBUG
	void* result = NULL;

	result = (void*)pvPortMalloc(size);
	
	//trace_malloc(result, size, (void *)__builtin_return_address(0));

	return	result;
	
	#else
	
	return(pvPortMalloc(size));
	
	#endif
}

void Ez_Free(void* pv)
{
	#ifdef CONFIG_MEM_DEBUG
	
	vPortFree(pv);
	trace_free(pv, (void *)__builtin_return_address(0));

	#else
	
	vPortFree(pv);
	
	#endif
}

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐