前言
本文记录总结了笔者曾经参与的sc770x项目,基于 Trace32 Simulator 定位系统稳定性问题,这是 Lauterbach 公司推出的一款嵌入式系统调试工具,支持多种 CPU 和 RTOS 调试,拥有很强的扩展性,支持CMM脚本扩展。
工具的使用
一般死机会保存对应的 ass 和 mem文件,打开 Trace32 Simulator ,导入 sc770x_simulator.cmm 脚本,加载当前系统版本对应的 axf 符号表文件。此时会提示输入 mem 地址,根据死机时保存的 ass 文件确定为0,然后再选择对应的 mem 文件,一般直接能够看到从 thread_entry 到死机现场的函数回调。如果不幸没有看到完整的回调,那么就需要自己进行推导,类似下面的情况。
查看死机状态时 _tx_thread_current_ptr 的值,根据 tx_thread_name 得知当前是 T_MIDI Task 。
1 | _tx_thread_current_ptr = 0x01D8FADC -> ( |
当前 Task 的栈的起始地址是 0x027D68B8,结束地址是 0x027D7CB3 ,查看对应地址的 data.dump 内存窗口。
1 | tx_stack_start = 0x027D68B8, |
从 ass 文件中查看死机前 SVC 模式下寄存器的值,以 R13_SVC 为起始地址,按照函数调用的堆栈原则推导。
1 | > SVC mode: |
找到以0x08开头的函数地址,将其作为 R14,函数压栈结束地址作为 R13,正向推栈的方法是 tx_stack_end -> R13_SVC
,反向推栈的方法是 R13_SVC -> tx_stack_end
。当我们按照这个方法,就能获得完整的 callback 。
1 | -001|MIDI_Core_Calculation_dls_stereo( |
我们可以根据 callback 获得死机步骤场景和参数(但参数并不一定准确),推导时如果无法推进就进行参数代入试验和猜测再反向验证的方法,以便使到 callback 向更深推进,以达到死机现场,便于进一步的分析问题。
问题分析
一般根据 ass 文件提示的 assert 信息,能够大概了解死机的原因。
- 最基本的 assert 信息,根据打印信息可以知道问题代码,直接分析原因。
1 | File:nfc_drv_v1.c Line: 501 PASSERT(0) > NFC timeout happened!f=4,e=0 |
内存泄漏
一般是使用 SCI_ALLOC 申请内存后,却没有调用 SCI_FREE 释放对应内存,导致内存泄漏。
Threadx 在内存管理中定义了 block pool 和 byte pool 两种概念,byte pool 设置固定大小的内存池,支持可变的内存申请释放,但是会存在碎片化问题,而block pool 设定不同大小的 block,方便快速申请释放,不会存在碎片化问题,性能上比 byte pool 会更有效率。
笔者曾根据 byte pool 和 block pool 分配的规则,编写了 mocor_byte_pool_list.cmm 和 mocor_block_pool_list.cmm 脚本,用来检查 heap 和 pool 两种内存空间的分配是否存在异常,有助于分析内存覆盖等问题。
- 通过 ArmLogel trace 定位
手机连接 ArmLogel 工具,通过 [SysInfo]->[Memory Status] 和[SysInfo]->[Memory Allocated Status] 将内存池信息和内存分配情况打印出来。
打开应用测试,然后通过相同方式保存内存信息。
如此重复多次,对比保存文件中的内存信息差异,确定是否有规律可循。如果存在内存泄漏,可通过
Allocated memory info
信息对比查找泄漏源。
- 通过工具 Assert 定位
出现内存问题时,查看 Assert 后的 ass 文件,打印的 Assert 信息的过程实际上是对当前系统处于 Assert 状态时的 Pool 内存和 Heap 内存信息检查的过程,通过观察内存信息输出的完整性检查,基本可以看出来系统的内存是否正常。
- Assert 后弹出界面有如下信息:
- 按5输出内存池 Pool 和 Heap 使用情况,如果有大批内存池耗光(Avail_Num 为0),有可能是内存泄漏问题。
- 按4输出内存池的详细分配信息,可以查找哪些文件在大量申请内存,观察规律是否存在内存泄漏。
内存越界
内存越界是由于编程不当导致的内存越界覆盖,实际使用的内存空间大于申请的内存范围,比较常见的是 memcpy 时,数据源太长,导致内存end_flag被覆盖。此类问题需要找到发生内存越界之前的内存块,仔细分析该内存块的使用序列,是否有可能造成内存越界访问。
block pool
比如下面这种 block pool 越界的情况:
1 | -000|TXAS_SaveMainReg( |
在释放 0x03FD908C 内存地址空间时,函数 osa_validate_buff_footer 检查 block pool 的时候发现找不到 end flag 导致 assert。
根据 threadx 内存分配原则,0x03FD94A8 地址内存的值应该是 0xF2F2F2F2,但是实际上是 0x003A0044。分析这个代码逻辑,发现原因是 SendMultiPicByBt 中 send_file_num 和 total_num 不相等导致内存越界。
byte pool
还有一种 byte pool 越界情况:
1 | -000|tx_byte_pool_search_ex( |
从死机处代码逻辑和 callback 中并没有看出问题,但是却在申请内存过程中,遍历 byte pool 出错。那么怀疑内存区域中存在异常,可能存在被覆盖的情况。使用 mocor_block_pool_list.cmm 脚本检查 byte pool 内存空间:
1 | byte_static_heap: 0x0B68984 |
0x268B9AC 处的内存节点正常,但是 0x268C410 处的内存节点已经异常,end_flag:0xAA 已经丢失,变成了一片非法数据。
按照函数 tx_byte_pool_search_ex 代码逻辑,最终会导致 0 地址访问,R5=0x80818081
,然后执行 ldr r0,[r5,#04]
,导致4字节对齐异常死机 (这个和 assert 文件中的错误提示也是匹配的:Fault address :0x80818085 )。
查看 0x268B9AC 地址空间的内存分配情况,如下:
分析 mmipicview_wintab.c 文件中内存分配和使用的代码,发现是 SendMultiPicByBT 函数中针对 send_file_info 分配的内存使用时存在越界操作,从而引发了上面的问题。
内存覆盖
导致内存覆盖原因很多,空指针的操作会操作 0 地址,相对比较容易检查,因为 0 地址一般是 DSP code 区域,可以通过BUSMonitor 辅助监控 code 区域,定位到覆盖代码段的源头。
野指针的操作比较复杂,可以利用 pool_list.cmm 脚本检查内存分配的完整性,也可以通过 dump memory 内容进行比对,寻找覆盖来源。
死机现场:
1 | File: tx_byta.c |
推导出 callback
1 | -000|TXAS_SaveMainReg( |
从 callback 能够看出,是在内存分配过程中出现问题,这里编译内存节点时出现异常,使用 mocor_byte_pool_list.cmm 进行检查:
根据内存分配器的规则查找,从 0x027DD46C 寻找下一个内存节点时出现问题,这里的内存被 0x21242124覆盖。
根据对应 assert 文件中的信息,ctrlmenu.c 分配的内存从0x027DC2BC 到 0x027DD46C,但是 0x027DD46C 开始的位置被覆盖了。
根据log记录,这段内存在死机前先是分配给了jpeg decode使用,通过代码逻辑我们可以看到 APP 发起的 Destroy 立刻返回并释放了内存,并没有等待 Set Event 的动作(解码 IMG_DEC 结束)。
1 | 2265207-4 37131 [MMIPIC]:HandlePicListWinMsg msg_id =f023 |
GUIANIM_DestroyHandle 没有等待 decode 执行JPEGDEC_DestoryHandle 停止底层解码的动作,提前释放内存,导致当前 decode 的动作继续使用了之前的内存,如下,通过 T_IMG_DEC task 的信息也可以看出来,target_ptr_=_0x027DC300 依然存在。
1 | -000|tx_thread_suspend( |
通过研究代码发现此问题的原因如下:
buffer target_ptr 的申请与释放是由应用层所做的,当应用层调用了函数 IMG_DEC_Remove_Handle 后,只是停止发送解码消息的进程,实际解码工作仍在进行,当解码完成后,会调用函数 JPEG_OutputData 将解码后的数据写入 target_ptr 所指内存,但上层应用此时已经释放了此内存,导致非法内存访问。需要修改代码流程,在调用 JPEG_OutputData 函数前,判断该图片是否被强制结束解码,如果是则不调用,否则则调用,从而将解码后的数据传递给上层应用。
另外发现 JVM callback 在图片解码 task 中调用导致 envent 状态错乱,导致 APP 发起的 Destroy 立刻返回并释放了内存。
死锁问题
死锁问题一般是因为互斥量的使用不当引发的问题,可能会导致界面不响应或看门狗复位等问题。
以看门狗复位为例,ass 文件有以下信息:
1 | File: watchdog.c |
对应 callback 如下:
1 | -000|TXAS_SaveMainReg( |
提示死机原因是 Task APP timeout
,那么分析代码是在 mmimain.c 中,函数 void APP_Task(uint32 argc, void *argv)
注册的看门狗没有及时喂狗。
watchdog_ptr = SWDG_RegTask("APP", 180000)
分析 APP_Task callback 如下,这里在获取 img_decoder_event 时被挂起。
1 | -000|tx_thread_suspend(?) |
而 img_decoder_event 应该在 T_IMG_DEC Task 中被释放,但是这个 Task 被信号量 JPEG_FREE_RES_SEMAP 挂起,参考下面的 callback:
1 | -000|tx_thread_suspend( |
通过 ass 文件,也能够看出 T_IMG_DEC Task 被 JPEG_FREE_RES_SEMAP 挂起。
1 | > JPEG_FREE_RES_SEMAP 0 |
根据这些 callback 继续分析代码,得到如下的结论:
应用窗体在丢失焦点时会发消息让 T_IMG_DEC 执行销毁流程,而 T_IMG_DEC拿到锁执行 JPEGDEC_DestroyHandle 销毁流程,这个执行过程中会释放 JPEG_FREE_RES_SEMAP;此时 T_JPEG_DECODER 从挂起状态解除,但是因为在 JPEGDEC_DestroyHandle 前面被销毁了,导致不能执行。两个 task 都不能执行,所以导致 timeout。
Task Queue Full问题
消息队列满现象为Assert提示:ASSERT: Error 0xb (The queue was full !),直接原因为接收消息的Task得不到执行,导致消息队列满,而在发送消息的任务检测到无法发送消息,直接报告消息队列满错误。
可能原因如下:
1.Task优先级太低,一直无法得到执行。
2.Task因为某些原因(比如死锁或信号量等)无法处理消息,可以分析代码逻辑。
3.中断处理太多,导致Task得不到执行,可以通过通过TaskAnalyzer工具分析中断原因。
4.关中断时间太长,导致Task得不到执行,尽量减少关中断的时间。
5.消息队列长度设置不正确,可以增加Queue Size。
此问题分析关键:首先找到无法处理消息的Task,而后逐条分析,包括当前Task队列的消息检查,也可以在Assert窗口输入命令“6”,输出Task的各项信息,寻找可用Queue数目为0的Task,这个Task就是问题点。
1 | -000|TXAS_SaveMainReg( |
结合代码能够看到,函数 MMISRV_CAMERAROLL_Download_Thumbnail()
发送消息给 T_P_APP_CAMERAROLL_TASK 时,发现该Task消息队列满。
查看ASS文件,发现 T_P_APP_CAMERAROLL_TASK 消息队列确实已满:
1 | Task_ID Name Tcb_Addr Current_PC Queue_All Queue_Avail |
查看T_P_APP_CAMERAROLL_TASK callback,如下:
1 | -000|tx_thread_suspend( |
通过TaskAnalyzer内存打点信息可以看出,T_P_APP以及网络相关高优先级的Task被频繁调度,导致低优先级的 T_P_APP_CAMERAROLL_TASK 得不到调度,通过分析T_P_APP_CAMERAROLL_TASK 消息类型,发现是T_P_APP一直在重复频繁发送如下三个消息:
1 | 0xAD14:HTTP_SIG_GET_CNF |
分析代码问题原因:CAMERAROLL_TASK 并发使用了12个Http,但是对应的Queue只有20个,并不能支持场景使用。最优解是增加Queue个数,同时降低Http并发个数。
栈溢出问题
栈溢出的可能原因如下:
1.栈空间分配太小,不足以满足大量局部变量的使用场景,应尽量使用堆区动态内存。
2.函数调用层次过多,或者陷入递归死循环当中。
3.栈空间内存异常,可能发生内存覆盖导致栈空间数据异常。
1 | -000|TXAS_SaveMainReg( |
根据上面callback的提示信息,T_P_APP Task存在栈溢出,tx_stack_ptr = 0x2459928
超出了进程的栈帧起始地址 tx_stack_start_=_0x02462728
。
1 | (TX_THREAD*)0x1E00DE0 = 0x01E00DE0 -> ( |
手动推导T_P_APP Task callback发现,存在000~003死循环导致栈溢出。
1 | -000|HandlePicListWinMsg( |
总结
通过MTBF和Monkey测试会暴露出各种问题,需要借助于丰富的调试方法进行分析定位。当然除了常规的调试手法,我们也借助AMBA Bus Monitor监控指定的代码段,包括boot、kernel、dsp等区域,以定位内存区域复写的异常情况。
1.MTBF测试明确了平均故障间隔时间,可以反映出产品的时间质量。
2.Monkey压力测试,保证产品的软硬件稳定性。
3.EUT Release版本测试,此时大部分问题已经收敛,该阶段问题不易复现,可能安排各种专项测试。