程序跑着跑着打印一串奇奇怪怪没见过的打印,就像下面的打印一样?
CPU Excep
tion: NO.2
r0: 0x00000014 r1: 0x18a70124 r2: 0x00001111 r3: 0x10020000
r4: 0x00000000 r5: 0x00000001 r6: 0x00000002 r7: 0x07070707
r8: 0x00000000 r9: 0x09090909 r10: 0x10101010 r11: 0x11111111
r12: 0x40000000 r13: 0x00000000 r14: 0x18b166a8 r15: 0x186d9c0a
r16: 0x16161616 r17: 0x47000000 r18: 0x3f800000 r19: 0x00000000
r20: 0xc0000000 r21: 0x40000000 r22: 0x00000000 r23: 0x00000000
r24: 0x40400000 r25: 0x12345678 r26: 0x12345678 r27: 0x12345678
r28: 0x12345678 r29: 0x12345678 r30: 0x12345678 r31: 0x12345678
vr0: 0x12345678 vr1: 0x00000000 vr2: 0x00000000 vr3: 0x00000000
vr4: 0x00000000 vr5: 0x00000000 vr6: 0x00000000 vr7: 0x00000000
vr8: 0x00000000 vr9: 0x00000000 vr10: 0x00000000 vr11: 0x00000000
vr12: 0x00000000 vr13: 0x00000000 vr14: 0x00000000 vr15: 0x00000000
vr16: 0x00000000 vr17: 0x00000000 vr18: 0x00000000 vr19: 0x00000000
vr20: 0x00000000 vr21: 0x00000000 vr22: 0x00000000 vr23: 0x00000000
vr24: 0x00000000 vr25: 0x00000000 vr26: 0x00000000 vr27: 0x00000000
vr28: 0x00000000 vr29: 0x00000000 vr30: 0x00000000 vr31: 0x00000000
vr32: 0x00000000 vr33: 0x00000000 vr34: 0x00000000 vr35: 0x00000000
vr36: 0x00000000 vr37: 0x00000000 vr38: 0x00000000 vr39: 0x00000000
vr40: 0x00000000 vr41: 0x00000000 vr42: 0x00000000 vr43: 0x00000000
vr44: 0x00000000 vr45: 0x00000000 vr46: 0x00000000 vr47: 0x00000000
vr48: 0x00000000 vr49: 0x00000000 vr50: 0x00000000 vr51: 0x00000000
vr52: 0x00000000 vr53: 0x00000000 vr54: 0x00000000 vr55: 0x00000000
vr56: 0x00000000 vr57: 0x00000000 vr58: 0x00000000 vr59: 0x00000000
vr60: 0x00000000 vr61: 0x00000000 vr62: 0x00000000 vr63: 0x00000000
epsr: 0xe4000341
epc : 0x186d9c12
不用紧张,你没有把板子搞坏了,只是程序跑挂了。下面我们就来一步一步的分析,我们掉进了什么坑里,怎么跳出来?
你需要知道的基础知识
下面介绍一些基础知识,如果你已经是老鸟,可以不用看这些。
几个重要的寄存器
这几个重要的寄存器都在上面的异常打印中打印出来了。
几个重要的文件
其中:
yoc.map 文件必须在 编译链接的时候通过编译选项生成,例如:CK的工具链的编译选项位 -Wl,-ckmap='yoc.map'
yoc.asm 文件可以通过elf 文件生成,具体命令为 csky-abiv2-objdump -d yoc.elf > yoc.asm
异常号
在我们的ck cpu架构里,不同的cpu异常会有不同的异常号,我们往往需要通过异常号来判断可能出现的问题。
异常号
| 说明
|
0
| 重启异常
|
1
| 未对齐访问异常
|
2
| 访问错误异常
|
3
| 除以零异常
|
4
| 非法指令异常
|
5
| 特权违反异常
|
6
| 跟踪异常
|
7
| 断点异常,地址观测异常
|
8
| 不可恢复错误异常
|
这些异常中,出现最多的是 1、2 号异常,4、7 偶尔也会被触发,3号异常比较好确认,其余基本不会出现。
开始填坑
了解完基础知识,我们就要开始填坑了,不管是谁挖的坑,总还是要填回去的。
连上GDB
如何连接GDB可以参考上一篇文章的内容
恢复现场
在GDB 使用 set 命令 将异常的现场的通用寄存器和 PC 寄存器设置回CPU中,便可以看到崩溃异常的程序位置了
(cskygdb)set $r0=0x00000014
(cskygdb)set $r1=0x18a70124
(cskygdb)set $r2=0x00001111
(cskygdb)set $r3=0x10020000
...
(cskygdb)set $r14=0x18b166a8
(cskygdb)set $r15=0x186d9c0a
...
(cskygdb)set $r30=0x12345678
(cskygdb)set $r31=0x12345678
(cskygdb)set $pc=$epc
不同的CPU 通用寄存器的个数有可能不相同,一般有 16个通用寄存器、32个通用寄存器,两种版本,我们只需要把通用寄存器,即 r 开头的寄存器 设置回CPU即可。
PC 寄存器 需要设置成 EPC, r14 r15 分别是 sp 寄存器和 lr寄存器。pc r14 r15 三个寄存器 是找回现场的关键寄存器,其余的通用寄存器是一些函数传参和函数内的局部变量。
设置完成以后可以通过 GDB bt 命令可以查看异常现场的栈
(cskygdb) bt
#0 0x186d9c12 in board_yoc_init () at vendor/tg6100n/board/init.c:202
#1 0x186d9684 in sys_init_func () at vendor/tg6100n/aos/aos.c:102
#2 0x186dfc14 in krhino_task_info_get (task=
, idx=, info=0x11)
at kernel/kernel/rhino/k_task.c:1081
Backtrace stopped: frame did not save the PC从 bt 命令 打印出来的栈信息,我们可以看到 异常点在 init.c 的 202 行上,board_yoc_init 函数内。
到这里,对于一些比较简单的错误,基本能判断出了什么问题。
如果没法一眼看出问题点,那我们就需要通过异常号来对应找BUG了。
通过异常号找BUG
程序崩溃后,异常打印的第一行就是CPU异常号。
CPU Exception: NO.2如上,我们示例中的打印是 2号异常。
2号异常是最为常见的异常,1号异常也较为常见。4号、7号一般是程序跑飞了,运行到了一个不是程序段的地方。3号异常就是除法除零了,比较好确认。其余的异常基本不会出现,出现了大概率也是芯片问题或者某个驱动问题,不是应用程序问题。
CPU Exception: NO.1
一号异常是访问未对齐异常,一般是一个多字节的变量从一个没有对齐的地址赋值或者被赋值。
例如:
uint32_t temp;
uint8_t data[12];
temp = *((uint32_t*)&data[1]);如上代码,一个 4字节的变量 temp 从 一个单字节的数组中取4个字节内容,这种代码就容易出现地址未对齐异常。这种操作在一些流数据的拆包组包过程比较常见,这个时候就需要谨慎小心了。
有些CPU 可以开启不对齐访问设置,让CPU可以支持从不对齐的地址去取多字节,这样就不会出现一号异常。但是为了平台兼容性,我们还是尽量不要出现这样的代码。
当然一号异常的出现也有可能是一个变量、一片内存被踩了导致的内存地址不对齐。
CPU Exception: NO.2
二号异常是访问错误异常,一般是访问了一个不存在的地址空间。
例如:
uint32_t *temp;
*temp = 1; 如上代码,temp 指针未初始化,如果直接给 temp指针指向的地址赋值,有可能导致二号异常,因为temp指向的地址是个随机值,该地址可能并不存在,或者不可以被写入。
二号异常也是最经常出现的异常,例如常见的错误有:
请注意你代码里的 memset、memcpy、malloc、free 、strcpy等调用。
大部分2号异常和1号异常的问题,异常的时候都不是第一现场了,也就是说异常点之前就已经出问题了。
比如之前就出现了 memcpy的 内存访问越界,内存拷贝超出变量区域了。memcpy的时候是不会异常的,只有当程序使用了这些被memcpy 踩了内存时,才会出现一号或二号异常。
这个时候异常点已经不是那个坑的地方了,属于“前人埋坑,后人遭殃”型问题。
如果是一些很快就复现的问题,我们可以通过GDB watch命令,watch那些被踩的内存或变量来快速的定位是哪段代码踩了内存。
如果是一些压测出现的问题,压测了2天,出了一个2号异常,恭喜你,碰到大坑了。类似这种,比较难复现的问题,watch已经不现实了。
结合异常现场GDB查看变量、内存信息和reiview代码逻辑,倒推出内存踩踏点,是比较正确的途径。
再有,就是在可疑的代码中加 log日志,增加压测的机器,构造缩短复现时间的case等一些技巧来加快BUG解决的速度。
CPU Exception: NO.4 CPU Exception: NO.7
四号异常是指令非法,即这个地址上的内容并不是一条CPU机器指令,不能被执行。
七号异常是断点异常,也就是这个指令是断点指令,即 bktp 指令,这是调试指令,一般代码不会编译生成这种指令。
这两种异常大概率是 指针函数没有赋值就直接跳转了,或者是代码段被踩了
例如:
typedef void (*func_t)(void *argv);
func_t f;
void *priv = NULL;
if (f != NULL) {
f(priv);
}如上代码,f 是一个 函数指针,没有被赋值,是一个随机值。直接进行跳转,程序就肯定跑飞了。
这种异常,一般epc地址,都不在反汇编文件 yoc.asm 中。
CPU Exception: NO.3
3号异常是除零异常,也是最简单、最直接的一种异常。
例如:
int a = 100;
int b = 0;
int c = a / b; 如上代码,b 变量位 0,除零就会出现 三号异常。
不用GDB也能找到异常点
有些时候无法使用GDB去查看异常点,或者搭环境不是很方便怎么办?
这个时候我们可以通过 反汇编文件和epc地址来查看,异常的函数。
打开yoc.asm 反汇编文件,在文件内搜索epc地址,就可以找到对应的函数,只是找不到对应的行号。
例如:
186d9b14 :
186d9b14: 14d3 push r4-r6, r15
186d9b16: 1430 subi r14, r14, 64
186d9b18: e3ffffc6 bsr 0x186d9aa4 // 186d9aa4
186d9b1c: 3001 movi r0, 1
186d9b1e: e3fe3221 bsr 0x1869ff60 // 1869ff60
186d9b22: e3fe4ca9 bsr 0x186a3474 // 186a3474
186d9b26: e3fffe7d bsr 0x186d9820 // 186d9820
...
186d9bfc: 1010 lrw r0, 0x188d1a50 // 186d9c3c
186d9bfe: e00c6aeb bsr 0x188671d4 // 188671d4
186d9c02: ea231002 movih r3, 4098
186d9c06: ea021111 movi r2, 4369
186d9c0a: b340 st.w r2, (r3, 0x0)
186d9c0c: 1410 addi r14, r14, 64
186d9c0e: 1493 pop r4-r6, r15
186d9c12: 9821 ld.w r1, (r14, 0x4)
186d9c14: 07a4 br 0x186d9b5a // 186d9b5a
186d9c14: 188d19c0 .long 0x188d19c0如上的汇编代码,根据异常的epc地址0x186d9c12,我们可以确认异常的函数发生在 board_yoc_init内。
使用 addr2line 命令可以找到程序代码位置。
addr2line -e yoc.elf 0x186d9c12
vendor/tg6100n/board/init.c:202
文章转载自:平头哥芯片开放社区 作者:小黄