维测即异常处理,在不同体系架构文档中对“异常”的定义存在不同,“异常”即Exception,在MIPS与ARM中的把interrupt、trap也都归类为Exception。而本文中的“异常”指的是代码出现了致命错误,导致系统异常崩溃而无法运行下去的状况。产生的直接原因可能是执行非法指令、访问非法内存等,间接原因可能是内存申请失败、代码跑飞、内存被踩、callback函数未注册等等。

维测能解决什么问题

简单的说,维测的价值在于可以缩短bug定位时间。如果一个bug出现导致系统异常后,用户可以不用连仿真器、不用加打印、不用打开gdb单步调试的情况下,可以快速找到bug原因,或者帮助用户指出可能的异常点,进而修复节省开发时间。

举例说明:

  1. 代码中访问了非法内存(比如:在不可写的地址处写了数据)导致系统崩溃,维测模块可以记录访问非法内存时的pc值,告诉用户挂在了哪一行;
  2. 代码跑飞了(pc=0),维测模块记录了函数调用的栈,并根据栈向上回溯可以找到A->B->C的函数调用过程;
  3. 用户内存申请时malloc 失败,维测模块可以记录用户此时申请了多少内存导致了内存池不够、此时还有多少字节内存可以供申请、用户是在哪个任务中申请的内存、从系统启动开始内存的申请情况等信息,帮助用户查看是否有组件申请了过大内存但没有释放等内存泄漏的情况;
  4. 已经有明显的踩内存现象,但是无法具体定位踩内存因,只知道一段内存被非法改写,复现现象不一致,定位相当耗时。维测模块可以提供接口,设置该段内存的属性为不可访问,从而制造memory访问异常,结合异常现场打印,快速定位踩内存的元凶;
  5. 在一次长达数小时的压测中(如linkkitapp、蓝牙配网等压测)出现了系统内存缓慢释放后内存耗尽、某个任务栈被踩等问题导致了系统奔溃,问题长时间压测才复现,维测模块可以在bug首次出现但系统还在正常运行的时候(内存第一次泄漏、任务栈第一次被踩等),主动触发异常告警,提醒用户系统存在隐患,并打印出异常现场信息供分析,不用等到数小时后才复现问题。

还有很多问题可以通过维测能力来帮助定位,更高级的维测功能也在持续开发中。

AliOS Things 维测组件提供的能力

AliOS-Things提供了2部分维测能力:

  1. 维测模块 --- 组件名为debug,组件位于core/debug路径下,功能是设备端的异常接管和异常log打印输出。
  2. 维测解析工具 --- 工具名为core_dump,工具位于components/utility/debug_tools路径下,是PC端基于Python的解析异常log的工具,生成更为详细的异常报告。

当系统异常后,可由AliOS-Things接管异常处理。维测组件会打印丰富的异常现场信息,协助用户定位问题。

1. AliOS Things维测模块提供的异常现场信息(设备端能力)

!!!!!!!!!! Exception  !!!!!!!!!!
========== Regs info  ==========          异常现场寄存器信息
PC       0x401111AA            
PS       0x00060130
A0       0x801111CA
A1       0x3FFDC830
A2       0x00000017
A3       0x3FFC2C18
A4       0xFFFFFFFF
A5       0x00000287
A6       0x00000002
A7       0x00000044
A8       0x00000001
A9       0x12345678
A10      0x00000001
A11      0x3FFCACFC
A12      0x00000001
A13      0x3FFDC510
A14      0x3FFDC510
A15      0x00000001
SAR      0x00000001
EXCCAUSE 0x0000001D
EXCVADDR 0x12345678

========== Stack info ==========         异常现场栈信息
stack(0x3FFDC830): 0x03020100 0x00000044 0x00000002 0x00000000 
stack(0x3FFDC840): 0xFFFFFFFF 0x00000287 0x00000002 0x00000044 
stack(0x3FFDC850): 0x4008C1E4 0x3FFDC880 0x3FFC09C0 0x00000000 
stack(0x3FFDC860): 0x8008D555 0x3FFCE680 0x3F4010C3 0x4008D50C 
stack(0x3FFDC870): 0x00000000 0x3FFDC8A0 0x00000000 0x00000000 
stack(0x3FFDC880): 0x00000000 0x00000000 0x00000000 0x00000000 
stack(0x3FFDC890): 0x00000000 0x00000000 0x00000000 0x00000000 
stack(0x3FFDC8A0): 0x00000000 0x00000000 0x3FFDC8AC 0x00000000 
stack(0x3FFDC8B0): 0x00000000 0x00000000 0x00000000 0x00000000 
stack(0x3FFDC8C0): 0x00000000 0x00000000 0x00000000 0x00000000 
stack(0x3FFDC8D0): 0x00000000 0x00000000 0x00000000 0x00000000 
stack(0x3FFDC8E0): 0x00000000 0x00000000 0x00000000 0x00000000 
stack(0x3FFDC8F0): 0x00000000 0x00000000 0x00000000 0x00000000 
stack(0x3FFDC900): 0xFEFEFEFE 0x40111C80 0x3FFDA8F0 0x00000320 
stack(0x3FFDC910): 0x00000001 0x00000012 0x00000000 0x00000000 
stack(0x3FFDC920): 0x00000000 0x00000000 0x00000000 0x00000000 

========== Call stack ==========
======= Call stack Begin =======         栈回溯信息,可以得出函数调用过程
backtrace : 0x401111AA 
backtrace : 0x401111C7 
backtrace : 0x4008D521 
backtrace : ^task entry^
======== Call stack End ========

========== Heap Info  ==========        内存信息,可以看出内存申请了多少,还剩多少
-----------------------------------------------------------
[HEAP]| TotalSz    | FreeSz     | UsedSz     | MinFreeSz  |
      | 0x0002D418 | 0x0001C868 | 0x00010BB0 | 0x0001A4D0 |
-----------------------------------------------------------
[POOL]| PoolSz     | FreeSz     | UsedSz     | BlkSz      |
      | 0x00002000 | 0x00001B60 | 0x000004A0 | 0x00000020 |
-----------------------------------------------------------

========== Task Info  ==========        任务状态信息,可以看出任务栈是否过小
--------------------------------------------------------------------------
TaskName             State    Prio       Stack      StackSize (MinFree)
--------------------------------------------------------------------------
dyn_mem_proc_task    PEND     0x00000009 0x3FFC792C 0x00000400(0x000002C0)
idle_task            RDY      0x0000003D 0x3FFC7D50 0x00000400(0x0000029C)
DEFAULT-WORKQUEUE    PEND     0x00000014 0x3FFC8230 0x00000C00(0x00000ADC)
timer_task           PEND     0x00000005 0x3FFC6B40 0x00000C00(0x00000ACC)
esp_timer            PEND     0x00000006 0x3FFCAE80 0x00001000(0x00000E4C)
ipc0                 PEND     0x00000002 0x3FFCC0B0 0x00000400(0x000002CC)
tcp/ip               PEND     0x00000007 0x3FFCED68 0x00000C00(0x000009E4)
eventTask            PEND     0x0000000A 0x3FFD0798 0x00001200(0x00000B5C)
wifi                 PEND     0x00000004 0x3FFD2200 0x00000E00(0x000005EC)
main                 RDY      0x00000020 0x3FFDA900 0x00002000(0x00001AD4)
cli                  PEND     0x0000003C 0x3FFDCC40 0x00000800(0x0000056C)

========== Queue Info ==========       queue使用信息
-------------------------------------------------------
QueAddr    TotalSize  PeakNum    CurrNum    TaskWaiting
-------------------------------------------------------

======== Buf Queue Info ========       buf queue使用状态,可以看出哪个任务在bufqueue消息
------------------------------------------------------------------
BufQueAddr TotalSize  PeakNum    CurrNum    MinFreeSz  TaskWaiting
------------------------------------------------------------------
0x3FFC67C0 0x000001E0 0x00000000 0x00000000 0x000001E0 timer_task          
0x3FFCEC78 0x00000040 0x00000000 0x00000000 0x00000040 tcp/ip              
0x3FFD0188 0x00000600 0x00000000 0x00000000 0x00000600 eventTask           
0x3FFD1B78 0x00000640 0x00000000 0x00000000 0x00000640 wifi
                
=========== Sem Info ===========      信号量使用状态,可以看出哪个任务在等待信号量
--------------------------------------------
SemAddr    Count      PeakCount  TaskWaiting
--------------------------------------------
0x3FFC6798 0x00000000 0x00000000 dyn_mem_proc_task   
0x3FFC8208 0x00000000 0x00000000 DEFAULT-WORKQUEUE   
0x3FFCAE48 0x00000000 0x00000000 esp_timer           
0x3FFCC040 0x00000000 0x00000000                     
0x3FFCC078 0x00000000 0x00000000 ipc0                
0x3FFCE920 0x00000001 0x00000001                     
0x3FFCE958 0x00000000 0x00000000                     
0x3FFCE990 0x00000000 0x00000000                     
0x3FFCE9C8 0x00000001 0x00000001                     
0x3FFCEA00 0x00000000 0x00000001                     
0x3FFCEBC8 0x00000000 0x00000000                     
0x3FFCEC00 0x00000000 0x00000000 cli                 
0x3FFCFA18 0x00000000 0x00000000                     
0x3FFCFA50 0x00000001 0x00000001                     
0x3FFD21C8 0x00000000 0x00000001                     
0x3FFDA858 0x00000000 0x00000000                     
!!!!!!!!!! dump end   !!!!!!!!!!

2. 维测解析工具的使用(PC 端能力)

维测解析工具core dump在tools/debug_tools路径下,在路径下有详细的使用方法介绍README,这里简单说明使用方法:

python coredump.py log helloworld@esp32devkitc.elf

其中:log --- 上面系统异常时的串口打印输出,可以拷贝到一个文件中,文件名任意,这里取名为log.elf --- 此时系统对应的elf文件

维测解析工具输出如下(部分):

  1. 异常原因可能解释:

    ********** Alios Things Exception Core Dump Result **********
    
    ========== Show Exc Regs Info  ==========
    EXCCAUSE : 0x0000001D
    EXCADDR  : 0x12345678
    
    A load/store referenced a page mapped with an attribute that does not permit
    Potential reasons:
    1. Access to Cache after it is turned off
    2. Wild pointers

    可见是访问了非法内存。

  2. 栈回溯 backtrace,即可清晰看到发生异常的函数调用过程: app_entry -- > application_start --->test_panic

    并且指出了函数代码的路径和行号。如图:

    ======= Call stack Begin =======          
    backtrace : 0x401111AA 
    test_panic at /home/yx170385/code/aos/app/example/helloworld/helloworld.c:20
    
    backtrace : 0x401111C7 
    application_start at /home/yx170385/code/aos/app/example/helloworld/helloworld.c:37
    
    backtrace : 0x4008D521 
    app_entry at /home/yx170385/code/aos/platform/mcu/esp32/bsp/entry.c:30
    
    backtrace : ^task entry^

  3. 指出异常发生在哪个任务中

========== Show Task Info  ==========
Crash in task : main

为了方便用户更好的使用,维测工具也在不断的升级中。

当前主线版本支持维测功能的情况

从AliOS Things 2.0开始,维测功能上线。打开维测会使得ROM大小增加2K左右,RAM基本无变化。

在当前主线上,默认打开维测功能的板子有以下几款“精品芯片”计划的板子:

1 developerkit
2 stm32f429 网关应用
3 esp32 智能语音应用
4 esp8266
5 pca10040(nrf52832) 手环
6 amebaz_dev (RTL8710) 连接应用
7 mk3080 (RTL8710) 连接应用
8 uno-91h(RDA5981x) 连接应用
9 bk7231u 智能语音应用

打开维测功能的方法(出临时版本使用)

  1. 在2.1及后续版本上打开维测的方法:

当前的ARM和Xtensa架构的芯片已经完成基本维测功能的适配,覆盖了绝大多数的板子。维测功能默认是关闭的,如果需要打开,采用menuconfig配置的方法,步骤如下:

  1. aos make menuconfig
  2. 选择kernel --- Debug & Cpuusage Support
  3. 将“Enable debug panic feature” 和 “Enable stack backtrace feature”选中,保存退出
  4. aos make

请注意: 当前最新版本3.1上支持了多种编译形态,即:

aos make---- 默认版本即 release版本,有基本维测功能,异常后自动重启,不会卡住

aos make xxxx BUILD_TYPE=inspect---- 为全维测版本,增加了异常后cli 接管、栈溢出检测等功能,并且不会重启,调试的时候请选择这个版本

  1. 在rel_2.0 上打开维测的方法:

编辑 kernel/rhino/debug/include/k_dftdbg_config.h将下面2个宏的值改为1重新编译

打开维测的宏后,编译出来的debug组件有3K多的字节,才是正常的,如图所示:

维测定位方法及API

在大多数情况下,使用维测组件及维测解析工具即可解决问题,维测接口api只在内部调试定位问题时使用,暂时无 aos 对外api,这里也总结了4个维测接口,用户可根据实际情况使用。

API 列表

debug_mm_overview() 内存堆信息状态显示
debug_task_overview() 任务状态显示
debug_backtrace_now() 调用栈回溯显示
debug_memory_access_err_check(addr, size, mode) 通过mpu监控一段内存,定位踩内存-栈溢出等场景使用

API 详情

debug_mm_overview()定义描述

描述 内存堆信息状态显示
入参 int *print_func -- 打印函数,可直接输入NULL
返回值

此函数会打印堆的相关统计,如下所示:

========== Heap Info  ==========
-----------------------------------------------------------
[HEAP]| TotalSz    | FreeSz     | UsedSz     | MinFreeSz  |
      | 0x0004A838 | 0x00047E50 | 0x000029E8 | 0x00047E50 |
-----------------------------------------------------------
[POOL]| PoolSz     | FreeSz     | UsedSz     | BlkSz      |
      | 0x00002000 | 0x00001E00 | 0x00000200 | 0x00000020 |
-----------------------------------------------------------

上面统计分成两部分,HEAP与POOL。HEAP是总的统计,POOL是HEAP的一部分。

HEAP与POOL的区别是,当用户使用

aos_malloc(size)

来分配内存的时候,size若小于32字节(RHINO_CONFIG_MM_BLK_SIZE,在k_config.h中定义),malloc会在POOL上固定分配32字节内存,反之则在HEAP上分配用户定义size的内存。

HEAP中的内容含义:

  • TotalSz,堆的总大小。
  • FreeSz,当前堆的空闲大小。
  • UsedSz,当前堆的使用量,即UsedSz = TotalSz – FreeSz。
  • MinFreeSz,堆空闲历史最小值,即TotalSz – MinFreeSz 便是堆历史使用量峰值。

出异常时,可以利用该信息大致判断堆是否出现空闲内存不足的问题。

调用示例

用户代码中,通过debug_mm_overview(NULL)接口可以主动打印heap消耗情况。这里给出一个周期性打印heap消耗的方式:

void krhino_tick_hook(void)
{
    //添加下面的代码
    static int s_cnt;

    if ( s_cnt++ % RHINO_CONFIG_TICKS_PER_SECOND == 0 )
    {
        debug_mm_overview(NULL);
    }
}

使用krhino_tick_hook()钩子函数需要开启RHINO_CONFIG_USER_HOOK

这样就可以让系统每秒都打印一次heap占用统计,来判断是否出现空闲内存不足、内存泄漏等问题

debug_task_overview()

定义描述

描述 任务状态信息显示
入参
返回值

此函数会打印堆的相关统计,示例如下所示:

--------------------------------------------------------------------------
TaskName             State    Prio       Stack      StackSize (MinFree)
--------------------------------------------------------------------------
dyn_mem_proc_task    PEND     0x00000006 0x200047A8 0x00000400(0x00000328)
idle_task            RDY      0x0000003D 0x200043F8 0x00000320(0x00000288)
DEFAULT-WORKQUEUE    PEND     0x00000014 0x200036D4 0x00000C00(0x00000B44)
timer_task           PEND     0x00000005 0x20003154 0x000004B0(0x000003B4)
aos-init             PEND     0x00000020 0x20000EE0 0x00001800(0x000014D8)
cli                  PEND     0x0000003C 0x20008CB8 0x00000800(0x00000688)

上面打印出系统当前共有dyn_mem_proc_task、idle_task、default-workqueue、timer_task、aos_init、cli 共6个任务每个任务的当前状态、优先级、任务栈以及栈的使用情况,若MinFree显示为0,则该任务很可能出现栈溢出情况,建议修改任务创建时的栈大小。

调用示例可将debug_task_overview() 根据需要加在代码中,观察任务信息。若使能了cli,cli中也有类似的打印。

debug_backtrace_now()

定义描述

描述 调用栈过程显示
入参
返回值

调用示例

用户可主动调用该函数,特别是在导致系统异常的怀疑点处调用,当代码执行到该函数时,会打印出栈回溯信息 ,示例如下所示:

========== Call stack ==========
......
backtrace : 0x08009B2A
backtrace : 0x0800A06C
backtrace : ^task entry^

栈回溯结果可以从下向上关注,最底部为^task entry 表示异常发生在任务中,为 interrupt^表示异常发生在中断处理中。之后根据编译工具链提供的arm-none-eabi-addr2line工具,输入地址与编译出的elf文件,可以找到对应C代码的位置。 举例:

d:\git\aos\out\helloworld@developerkit\binary>arm-none-eabi-addr2line -e helloworld@developerkit.elf 0x0800A06C
D:\git\aos/kernel\rhino\core/k_idle.c:59

d:\git\aos\out\helloworld@developerkit\binary>arm-none-eabi-addr2line -e helloworld@developerkit.elf 0x08009B2A
D:\git\aos/kernel\rhino\core/k_tick.c:13

根据以上C代码信息,分析可能的错误。

debug_memory_access_err_check()

注意: 只使用于 ARM cortex M,并且硬件带有MPU的处理器

定义描述

描述 通过mpu监控一段内存,定位踩内存、栈溢出等场景使用
入参 addr_start : 需要监控内存的起始地址
addr_size: 需要监控内存的大小,最小为32字节,最大为2G(注意size需要被addr_start整除)
mode : 访问模式。0 -- 禁止访问;非0 -- 只读访问
返回值

调用示例

/*踩内存场景*/
extern void debug_memory_access_err_check(unsigned long addr_start, unsigned long addr_size, unsigned int mode);

/*监控0x20008000起始处,大小为0x400(1K字节)的一段内存,设置该内存不可访问
  若该段内存被访问,则直接触发异常*/
debug_memory_access_err_check(0x20008000, 0x400, 0);

适用场景:已经明确有明显的踩内存现象,但是无法具体定位踩内存根因,只知道一段内存被非法改写, 复现现象不一致,定位相当耗时。

使用方法: 将这段内存通过上面接口设置地址访问权限,一旦非法访问立即异常,通过解析工具可快速定位。

/*栈溢出场景*/
/*在kernel/rhino/k_stats.c 的krhino_stack_ovf_check 中可增加下面代码,定位栈溢出*/

void krhino_stack_ovf_check(void)
{
    cpu_stack_t *stack_start;
    uint8_t      i;

    stack_start = g_active_task[cpu_cur_get()]->task_stack_base;

    for (i = 0; i < RHINO_CONFIG_STK_CHK_WORDS; i++) {
        if (*stack_start++ != RHINO_TASK_STACK_OVF_MAGIC) {
            k_err_proc(RHINO_TASK_STACK_OVF);
        }
    }

    if ((cpu_stack_t *)(g_active_task[cpu_cur_get()]->task_stack) < stack_start) {
        k_err_proc(RHINO_TASK_STACK_OVF);
    }

  /*增加下面的代码*/
	/*set ready task stack_base(32Bytes) access mode for stack ovf*/
	extern void debug_memory_access_err_check(unsigned long addr_start, unsigned long addr_size, unsigned int mode);
  debug_memory_access_err_check((unsigned long)(g_preferred_ready_task[cpu_cur_get()]->task_stack_base), 0x20, 0);
}

适用场景: 怀疑任务栈溢出,栈底被破坏。

使用方法: 在任务切换的栈溢出检测中增加上述接口,将栈低开始的32字节通过mpu保护起来,一旦出现栈溢出立即异常,通过解析工具可快速定位。

利用mpu 实现栈溢出的原理可参见下面的ppt:利用MPU实时栈溢出检测方法.pdf

常用技巧

上面几种情况的信息显示,都可以在系统发生异常的时候打印出来,用户也可以主动触发异常,将出现错误时的现场打印出来。为调试时提供帮助。

主动触发异常方法:

以GCC下Cortex-M系列为例,可以通过下面的代码主动触发异常:

__asm__ __volatile__("udf 0":::"memory");

类似的,通过强行跑到0地址,也可以触发异常。

((void (*)())0)();

AliOS Things维测能力与竞品的对比

通过分析竞品(freertos、mbed os)的代码和实际测试竞品rtos在同样板子上的维测运行相结合的方法,得出了竞品维测能力的结论:

  1. freertos 维测能力几乎为0,没有系统异常后的现场分析。
  2. mbed os的panic处理方式如下,也只提供了通用regs,core regs保存,和部分出错信息打印。