维测即异常和错误处理,代码中出现bug,常导致系统异常崩溃而无法正常运行。维测组件将异常和错误现场展现出来,指导问题定位。异常产生的直接原因可能是执行非法指令、访问非法内存等,间接原因可能是内存申请失败、代码跑飞、内存被踩、callback函数未注册等等。
简单的说,维测的价值在于可以缩短bug定位时间。如果一个bug出现导致系统异常后,用户可以不用连仿真器、不用加打印、不用打开gdb单步调试的情况下,可以快速找到bug原因,或者帮助用户指出可能的异常点,进而修复节省开发时间。
举例说明:
.......
还有很多问题可以通过维测能力来帮助定位,更高级的维测功能也在持续开发中(参考后文维测亮点规划)。
AliOS-Things提供了2部分维测能力:
当系统异常后,可由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 !!!!!!!!!!
维测解析工具core dump在tools/debug_tools路径下,在路径下有详细的使用方法介绍README,这里简单说明使用方法:
python coredump.py log helloworld@esp32devkitc.elf
其中:
log --- 上面系统异常时的串口打印输出,可以拷贝到一个文件中,文件名任意,这里取名为log
.elf --- 此时系统对应的elf文件
维测解析工具输出如下(部分):
********** 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
可见是访问了非法内存。
栈回溯 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^
========== Show Task Info ==========
Crash in task : main
为了方便用户更好的使用,维测工具也在不断的升级中。
从Alios Things 2.0开始,维测功能上线。打开维测会使得ROM大小增加2K左右,RAM基本无变化。
在大多数情况下,使用维测组件及维测解析工具即可解决问题,维测接口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中的内容含义:
出异常时,可以利用该信息大致判断堆是否出现空闲内存不足的问题。
调用示例
用户代码中,通过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)();