1、定位思路参考
各种各样的错误现场,使用的手段不尽相同。
正常问题有较固定的模式去一层层剥开现象,找到本质;
还有一些问题,需要定位者仔细观察发现每一处不合乎逻辑(代码逻辑、内存逻辑等)可疑点,忽略任意一点可疑点,极大概率就会导致错过真相,含有疑点地去下结论,误判的可能性极高。
还有一些复杂问题,看似毫无逻辑性,有时需要跳出固定思维,换个层面或者角度;
总之,大家在定位问题中都会有一些自己的心得,遇到过各种各样的情况。
此处从内核底层软件角度出发,提供一些基本有效的定位手段和分析方法,给大家参考。
1.1、找致命错误或者异常现场
从内核系统角度,在运行中会检测内存的异常状态。
首先要判断系统出现哪种问题,如下面是内核检测最常见的两种出错状态:
发现内存问题,如内存释放时检测内存踩踏,内存申请时,发现空间不足,即会报“!! Fatal Error !!”;
系统出现异常后,内核会进入异常处理流程,串口log会输出“!! Exception !!”。
1.2、内存释放检测致命错误问题
- 看打印,找报错内存
WARNING, memory maybe corrupt!! 0x81a2c378
- 串口可以输入,输入命令查看内存状态
p 0x81a2c378 32 4 // 输入32个4字节大小数据
- 搜 !used !free关键字,其为内核检测出被踩内存
- 观察出问题内存的前一块,或者之前内存块,一般简单情况都是前一块尾部踩后一块内存头:
- 通过汇编或者基本的调用栈,分析前面一块内存使用情况
重点关注前一块内存申请后的操作,是否有memset、memcpy动作,相关代码逻辑是否有合法性判断。从汇编来看是最准确的代码逻辑,不容易像C代码一样,可能存在各种宏开关之类的误导。但是逻辑复杂,看汇编则不合理,需要大家一起检视代码确认。
也会存在内存直接使用越界的情况,比如结构体强转后越界访问,此时可以通过编译、PC-lint告警之类简单排查,也可以直接检视汇编或者C代码。
检视相关后一致认为前面的内存没有异常使用的情况下,这种可能就是二次现场或者飞踩的情况。不同情况因地制宜,参考下面几种推荐方法:
- 优先考虑加watchpoint监控内存;
- p前后的内存值,找规律,尤其是看内存被改写成何值,有时可以关联被谁误改写;
- 看当前的内存头状态,是否存在已经被释放即use-after-free的情况;
- 看任务栈是否溢出:任务栈溢出会导致各种奇怪的问题,看当前可用任务栈是否已经很少;
- 排查业务逻辑自身问题,尤其涉及模块代码规范性问题,异常状况分支处理
- 排查多核多任务资源保护问题;
- 加维测:在合理的位置、合适的判断条件加合适的维测,能达到让问题尽快暴露,输出更多有用信息的效果。
1.3、看任务栈sp是否溢出
内核中有任务栈溢出的检测,如果检测到栈溢出了,则会即使报错。但是由于不可能检测所有栈范围,用户的代码如果访问时,跨过了栈检测的魔术字位置,那系统就不能及时报错,从而导致后续奇怪的现场。
异常时,内核会自动打印出任务信息,用户也可以在串口输入tasklist来查看
- 对比当前SP的值,和任务栈范围
如当前 SP 0x4F8097A8
查看对应任务信息:
在任务栈的范围内则正常。
另外可以查看当前任务栈的MinFree,其表示任务栈使用过程中的最小剩余值,如果发现其很小,则基本上栈已经溢出了。
1.4、手动解栈
偶尔会遇到各种原因,现场调用栈没有打印出来。有栈解析遇到特殊情况的问题,也有可能pc非法等造成解析不全之类的。这时候需要人工去解调用栈。
其实方法很简单:
找到当前的SP值 => 找到当前任务栈的内容 => 将大于等于SP位置的内存都过滤 =>
找到函数执行轨迹。
1.5、Addr2line
如在ELF结构章节中提到的,可以通过addr2line命令快速关联某汇编和对应的.c位置
localhost: arm-none-eabi-addr2line -e ./helloworld@developerkit.elf 0x80021f8
/workspace/aos_gerrit_new/core/osal/aos/rhino.c:815
1.6、查看当前任务状态
tasklist
异常现场会自动打印当前任务状态,用户可以直接输入tasklist查看当前任务状态
1.7、查看某任务调用栈
taskbt 任务ID
1.9、查看任务/信号量/mutex状态
debug
如从上图可以看到,dyn_mem_proc_task任务在等待某个sem,处于pend状态。
1.9、查看任务CPU占有率
cpuusage
以此定位是否有任务超负荷占用CPU
1.10、查看内存值
关键时候,需要查看内存,通过p命令
下面命令打印32个4字节内存:
1.11、Dumpsys导内存
怀疑内存堆有问题,可以通过dumpsys
mm_info命令查看所有内存blk状态,然后再结合p内存命令定位。
1.12、强制异常 (fiq)
如果系统停止运行,怀疑死锁之类的问题,可以通过
$#@!
命令通过让CPU进入fiq打断现场,内核会将当前的各种状态快照输出来分子当前的状态,包括:通用寄存器现场、任务状态、内存状态等。
1.13、异常定位
非法的内存、指令访问会导致异常。内存被踩,如果没有及时检测魔术字错误,用户访问的时候也会触发异常。
下面以某一实际遇到的异常log为例:
- 看异常类型,最普遍的区分数据异常还是指令异常
- 看异常位置,核对异常状态寄存器和通用寄存器
异常PC:
-
kernel space exception
-
========== Regs info ==========
-
PC 0x4060E160
对应汇编:
4060e160: e5d4900f ldrb r9, [r4, #15]
查看当前r4值:
查看数据异常寄存器:
从上面看出,R4 = 0x4FFFFFF4,导致访问了 0x4FFFFFF4 + 15位置的内存异常。
那问题就变为R4为何不对。
- 分析导致异常寄存器的前后赋值关系在汇编中(此时C代码也可),查看r4的赋值:
(下面出错位置代码较长,有截取汇编上下文,关注下面加“//”部分)
-
4060e098 <scanSetRequestChannel>:
-
4060e098: e92d43f0 push {r4, r5, r6, r7, r8, r9, lr}
-
4060e09c: e24dd04c sub sp, sp, #76 ; 0x4c
-
4060e0a0: e1a07001 mov r7, r1
-
4060e0a4: e1a04002 mov r4, r2 //
-
***********省略*****************
-
4060e160: e5d4900f ldrb r9, [r4, #15] //
-
4060e164: e3a02010 mov r2, #16
-
4060e168: e0880206 add r0, r8, r6, lsl #4
-
4060e16c: e1a01004 mov r1, r4
-
4060e170: e2800028 add r0, r0, #40 ; 0x28
-
4060e174: e2866001 add r6, r6, #1
-
4060e178: ebfef704 bl 405cbd90 <memcpy>
-
4060e17c: e3a02020 mov r2, #32
-
4060e180: e28d1028 add r1, sp, #40 ; 0x28
-
4060e184: e1a00009 mov r0, r9
-
4060e188: ebffff87 bl 4060dfac <scanSetBit>
-
4060e18c: e2855001 add r5, r5, #1 //
指令4060e160位置为异常位置;
4060e0a4位置R4为从R2赋值,R2为scanSetRequestChannel的第三个参数;
- 看压栈现场
此时怀疑scanSetRequestChannel入参有问题,那么可以看这个函数调用过来的现场,从Call
stack找出上一层位置:
-
========== Call stack ==========
-
backtrace : 0x4060E160
-
backtrace : 0x405FBFB4 //
-
backtrace : 0x405F5558
-
backtrace : 0x40628B60
-
backtrace : ^task entry^
查看0x405FBFB4汇编
-
405fbf94: e2842f6b add r2, r4, #428 ; 0x1ac //
-
405fbf98: e5d43001 ldrb r3,
-
405fbf9c: e1a00005 mov r0, r5
-
405fbfa0: e58d7000 str r7,
-
405fbfa4: e59411a8 ldr r1,
-
-
-
-
-
-
-
-
-
-
-
-
指令位置405fbf94显示R2由R4 + 0x1ac所得,那么就需要找到R4,而我们知道R4一般会在被调用函数scanSetRequestChannel压栈处理,那么回头看scanSetRequestChannel的汇编开头
-
4060e098 <scanSetRequestChannel>:
-
4060e098: e92d43f0 push {r4, r5, r6, r7, r8, r9, lr} //
那么找到压栈位置,就可以找到R4的值,一般通过被压栈的LR位置来搜索,当前进入scanSetRequestChannel时LR的值是405fbfb8,可以在栈的上下文搜索(或者通过当前SP)405fbfb8,找到下面栈位置,并往前搜索7个单元,找到R4压栈的位置,显示R4=0x4F7FEE98。
-
stack(0x4F8097E8) : 0x00043753 0x8184D561 0x4F7FEE98 0x4F7FEE98 (R4) //
-
stack(0x4F8097F8) : 0x4F769498 0x4F7FEFA0 0x4F80FD88 0x00000000
-
stack(0x4F809808) : 0x4F78FF8C 0x405FBFB8(LR) 0x4F80FD88 0x4071B904
到目前为止,我们找到R4 = 0x4F7FEE98,这个值看着挺正常,而且和出错时R4 0x4FFFFFF4地址相去甚远。
这里我们可以推测一个结论,至少R4在传入的时候还是好的,是在scanSetRequestChannel函数内部运行时发生了问题,怀疑大量循环之后或者不合理的数据运算造成了问题。
- 观测当前LR(这一步可以在前面观察)
当前异常时LR一般是函数调用时候当前PC跳转前的下一条指令,会随着函数调用不断变化,因此从LR我们能定位当前处于哪个函数范围内。
观察到当前LR 0x4060E18C
对应汇编:
从这一点看出,LR已经在发生异常指令0x4060e160之后,推测确实是在进入了scanSetRequestChannel之后,在进行来回的循环之后才导致的异常。
那么此处有理由怀疑scanSetRequestChannel的R2入参正确的情况下,可能其他参数有问题,造成了R2循环累加的溢出。
同样按照分析R4在栈位置的原理,以及参考对应C代码,可以继续发现下面问题:
arChannel 是出问题的指针源头,u4ScanChannelNum是其索引,有极大的嫌疑有问题。
-
void scanSetRequestChannel(IN struct ADAPTER *prAdapter,
-
IN uint32_t u4ScanChannelNum,
-
IN struct RF_CHANNEL_INFO arChannel[],
-
IN uint8_t fgIsOnlineScan,
-
OUT struct MSG_SCN_SCAN_REQ_V2 *prScanReqMsg)
再次分析汇编
u4ScanChannelNum作为参数被保存在R7,根据上下文R7没有被修改。
-
4060e098 <scanSetRequestChannel>:
-
4060e098: e92d43f0 push {r4, r5, r6, r7, r8, r9, lr}
-
4060e09c: e24dd04c sub sp, sp, #76 ; 0x4c
-
4060e0a0: e1a07001 mov r7, r1 //
再次查看异常寄存器现场,发现R7果然是一个很大的值。
最后一公里:
至此只能说找到出问题的点,scanSetRequestChannel的参数u4ScanChannelNum有问题,至于为什么R7有问题,代码是否有合法性判断,还是被其他引起的,此为后话,最后一公里,往往会产生很多变化,需要在实践中继续积累经验,才能最终定位根因,而不能仅停留在表明的错误。
2、质量工作
大部分时候,由于项目周期较短等原因,很多时候忽略了基本的代码质量检查。这种做法,对于商用产品来讲基本上可能是致命的,也是极度危险的。
除了少部分由代码逻辑之类引起的,剩余的还有一大部分问题都是由于代码的不规范引起的。解决不规范的代码所消耗的精力,往往比通过实际联调定位解决问题来的高效得多。
而且在某段时间实际运行中能发现的问题总归是有限的,远不如彻底根治不规范代码引起的风险来的有效。
下面列出的质量工作能够极大提高代码和产品质量:
- 编译告警
- PC-LINT检查
- Coverity检查
- Fortify检查
- 详细覆盖全面的单元测试
- 完善的逻辑组合测试
- 顶层的黑盒测试
- 其他
3、参考文档
《ARM相关架构文档》
《ELF格式解析》
4、传送门
HaaS100开发调试系列 之 使用AliOS Things诊断调试组件定位Bug
开发者技术支持
如需更多技术支持,可加入钉钉开发者群,或者关注微信公众号
更多技术与解决方案介绍,请访问阿里云AIoT首页https://iot.aliyun.com/