当前位置: 首页 > 工具软件 > AliOS Things > 使用案例 >

Alios Things 维测模块介绍

曾沛
2023-12-01

维测是什么

维测即异常和错误处理,代码中出现bug,常导致系统异常崩溃而无法正常运行。维测组件将异常和错误现场展现出来,指导问题定位。异常产生的直接原因可能是执行非法指令、访问非法内存等,间接原因可能是内存申请失败、代码跑飞、内存被踩、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,是设备端的异常接管和异常log打印输出。
  2. 维测解析工具 --- 工具名为coredump,是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

可见是访问了非法内存。

  1. 栈回溯 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^
  1. 指出异常发生在哪个任务中
========== Show Task Info  ==========
Crash in task : main

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

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

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

维测定位方法及API(选用)

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

API 列表

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

API 详情

debug_mm_overview()
定义描述

函数原型debug_mm_overview()
描述内存堆信息状态显示
入参
返回值

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

========== 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()

定义描述

函数原型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()

定义描述

函数原型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_mpu_region_set()

定义描述

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

调用示例

extern void debug_mpu_region_set(unsigned long addr_start, unsigned long addr_size, unsigned int mode);

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

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

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

常用技巧

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

主动触发异常方法:

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

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

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

((void (*)())0)();
 类似资料: