芯片开放社区
直播中

龙献益

7年用户 992经验值
私信 关注
[技术讨论]

如果遇到CPU Exception,我该怎么办

程序跑着跑着打印一串奇奇怪怪没见过的打印,就像下面的打印一样?


CPU Exception: 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
不用紧张,你没有把板子搞坏了,只是程序跑挂了。下面我们就来一步一步的分析,我们掉进了什么坑里,怎么跳出来?

你需要知道的基础知识

下面介绍一些基础知识,如果你已经是老鸟,可以不用看这些。

几个重要的寄存器

  • pc:程序计数器,它是一个地址指针,指向了程序执行到的位置
  • sp:栈指针,它是一个地址指针,指向了当前任务的栈顶部,它的下面存了这个任务的函数调用顺序和这些被调用函数里面的局部变量。在我们的cpu core框架里,它对应了 R14 寄存器


  • lr:连接寄存器,它也是一个地址指针,指向子程序返回地址,也就是说当前程序执行返回后,执行的第一个指令就是lr寄存器指向的指令,在我们的cpu core框架里,它对对应了 R15 寄存器
  • epc:异常保留程序计数器,它是一个地址指针,指向了异常时的程序位置,这个寄存器比较重要,出现异常后,我们就需要通过这个寄存器来恢复出现异常时候的程序位置。


  • epsr:异常保留处理器状态寄存器,它是一个状态寄存器,保存了出异常前的系统状态。

这几个重要的寄存器都在上面的异常打印中打印出来了。

几个重要的文件

  • yoc.elf:保存了程序的所有调试信息,GDB调试时必须用到该文件,编译完程序后务必保留包文件。
  • yoc.map:保存了程序全局变量,静态变量,代码的存放位置及大小。


  • yoc.asm:反汇编文件,保存了程序的所有反汇编信息。这些文件都保存在每个solutions目录中。如果使用剑池CDK开发,则位于项目的Obj目录中。


其中:

  • 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指向的地址是个随机值,该地址可能并不存在,或者不可以被写入。

二号异常也是最经常出现的异常,例如常见的错误有:

  • 内存访问越界
  • 线程栈溢出


  • 野指针赋值
  • 重复释放指针(free)

请注意你代码里的 memsetmemcpymallocfreestrcpy等调用。


大部分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
文章转载自:平头哥芯片开放社区 作者:小黄

更多回帖

发帖
×
20
完善资料,
赚取积分