上一章:智能语音组件适配指南 | 《无需从0开发 1天上手智能语音离在线方案》第六章>>>
基本调试指南
1. 使用串口调试
1.1 用内置串口命令调试
YoC支持丰富的串口命令,通过串口命令可以完成很多调试操作。系统支持串口命令介绍如下:
help
> help
help : show commands
ping : ping command.
ifconfig : network config
date : date command.
ps : show tasks
free : show memory info
sys : sys comand
log : log contrtol
iperf : network performance test
kv : kv tools
输入 help 命令,可以查看当前所有支持命令:
ps 命令可以打印出当前系统所有的线程状态,每项含义介绍如下:
部分信息详细说明如下:
• 线程状态有ready、pend、suspend、sleep、deleted
– ready:表示当前线程已经等待被调度,系统的调度原则是:若优先级不同则高优先级线程运行,优先级相同则各个线程时间片轮转运行
– pend:表示当前线程被挂起,挂起原因是线程在等待信号量、互斥锁、消息队列等,例如调用:aos_sem_wait,aos_mutex_lock 等接口,线程就会被挂起并置成pend状态。如果是信号量等待时间是forever,则left tick 的值为 0;如果有超时时间,则 left tick 的值就是超时时间,单位为毫秒
– suspend:表示当前线程被主动挂起,就是程序主动调用了 task_suspend 函数
– sleep:表示当前线程被主动挂起,就是调用了 aos_sleep 等睡眠函数, left tick 的值即表示 睡眠的时间
– deleted:当前线程已经被主动删除,也就是调用 krhino_task_del函数
• %CPU 状态只有在 k_config.h 文件中 RHINO_CONFIG_HW_COUNT和RHINO_CONFIG_TASK_SCHED_STATS宏被设置 1 的时候才会出现。
• 第一行 CPU USAGE: 640/10000 表示,当前系统的整体负载,如上示例,系统的CPU占有率是 0.64%
free
> free
total used free peak
memory usage: 5652536 605316 5047220 1093576
free 命令可以使用输出当前系统的堆状态,其中:
• total 为 总的堆的大小
• used 为 系统使用的 堆大小
• free 为 系统空余的 堆大小
• peak 为 系统使用的 堆最大空间
单位为 byte
>free mem
------------------------------- all memory blocks ---------------------------------
g_kmm_head = 1829bfc8
ALL BLOCKS
address, stat size dye caller pre-stat point
0x1829cb20 used 8 fefefefe 0x0 pre-used;
0x1829cb38 used 4128 fefefefe 0xbfffffff pre-used;
0x1829db68 used 1216 fefefefe 0x180190b6 pre-used;
0x1829e038 used 2240 fefefefe 0x180190b6 pre-used;
0x1829e908 used 4288 fefefefe 0x180190b6 pre-used;
0x1829f9d8 free 592 abababab 0x180aaa6d pre-used; free[ 0x0, 0x0]
0x1829fc38 used 40 fefefefe 0x180cb836 pre-free [0x1829f9d8];
0x1829fc70 used 40 fefefefe 0x180cb836 pre-used;
0x1829fca8 used 18436 fefefefe 0x1810448d pre-used;
0x182a44bc used 40 fefefefe 0x180cb836 pre-used;
...
0x183a5ce0 used 16 fefefefe 0x1801d477 pre-used;
0x183a5d00 used 40 fefefefe 0x1801d477 pre-used;
0x183a5d38 used 12 fefefefe 0x1801a911 pre-used;
0x183a5d54 used 32 fefefefe 0x18010d40 pre-used;
0x183a5d84 used 4288 fefefefe 0x180190b6 pre-used;
0x183a6e54 free 4559244 abababab 0x18027fd9 pre-used; free[ 0x0, 0x0]
0x187ffff0 used sentinel fefefefe 0x0 pre-free [0x183a6e54];
----------------------------- all free memory blocks -------------------------------
address, stat size dye caller pre-stat point
FL bitmap: 0x10f4b
SL bitmap 0x84
-> [0][2]
0x18349b88 free 8 abababab 0x1802a1b1 pre-used; free[ 0x0, 0x0]
-> [0][7]
0x182df2f8 free 28 abababab 0x0 pre-used; free[ 0x0, 0x0]
-> [0][25]
0x182df3c8 free 100 abababab 0x18010ea5 pre-used; free[ 0x0, 0x0]
...
0x182b5704 free 160204 abababab 0x1804fe55 pre-used; free[ 0x0, 0x0]
SL bitmap 0x4
-> [16][2]
0x183a6e54 free 4559244 abababab 0x18027fd9 pre-used; free[ 0x0, 0x0]
------------------------- memory allocation statistic ------------------------------
free | used | maxused
5047040 | 605496 | 1093576
-----------------alloc size statistic:-----------------
[2^02] bytes: 0 |[2^03] bytes: 1350 |[2^04] bytes: 398770 |[2^05] bytes: 29121 |
[2^06] bytes: 408344 |[2^07] bytes: 396962 |[2^08] bytes: 350 |[2^09] bytes: 231 |
[2^10] bytes: 55 |[2^11] bytes: 38 |[2^12] bytes: 396677 |[2^13] bytes: 1410 |
[2^14] bytes: 14 |[2^15] bytes: 16 |[2^16] bytes: 0 |[2^17] bytes: 4 |
[2^18] bytes: 17 |[2^19] bytes: 0 |[2^20] bytes: 0 |[2^21] bytes: 0 |
[2^22] bytes: 0 |[2^23] bytes: 0 |[2^24] bytes: 0 |[2^25] bytes: 0 |
[2^26] bytes: 0 |[2^27] bytes: 0 |
free mem 命令可以打印出堆内各个节点的细节信息 整个打印信息被分成 4个部分
• 第一部分为 系统所有 堆节点,包含了 节点的地址、大小、占用状态、调用malloc的程序地址等
• 第二部分为 当前系统 空置的 堆节点,信息与第一部分相同,只是单独列出了free的节点,可以观察系统的内存碎片情况
• 第三部分为 系统的总体堆内存使用情况,和 free 命令打印出的信息相同
• 第四部分为 堆节点的大小统计,与2的次方为单位进行划分
>free list
total used free peak
memory usage: 5652536 605316 5047220 1093576
0: caller=0xbffffffe, count= 1, total size=4128
1: caller=0x180190b6, count=25, total size=85696
2: caller=0x180aaa6c, count= 1, total size=592
3: caller=0x180cb836, count= 3, total size=120
4: caller=0x1810448c, count= 1, total size=18436
5: caller=0x18010a68, count=39, total size=1716
6: caller=0x18014548, count= 8, total size=580
7: caller=0x18054dda, count= 1, total size=1028
...
52: caller=0x18010d40, count= 2, total size=64
53: caller=0x1801d5b8, count= 3, total size=72
54: caller=0x1801d476, count= 6, total size=196
55: caller=0x1801d5ac, count= 3, total size=48092
56: caller=0x1801a910, count= 1, total size=12
57: caller=0x18027fd8, count= 1, total size=4559244
free list 是另一种形式的堆内存使用统计,统计了程序内各个malloc的调用并且还没有free的次数。 这个统计信息对于查找内存泄露非常有帮助。多次输出该命令,若 count 的值出现了增长,则可能有内存泄露的情况出现。
以上命令的 caller 信息,我们可以通过 在 yoc.asm 反汇编文件查找函数来确认具体的调用函数。
注意:free mem和free list只有在开启CONFIG_DEBUG_MM和CONFIG_DEBUG时才能使用,因为它需要占用一些内存空间用于存放这些调试信息。
sys
具体显示的信息如下:
其中 sys app 和sys id 两个命令是在需要FOTA升级的时候才会使用到,一般是OCC网站颁发的信息,不可更改,如果没有走过FOTA流程一般为空。其余的版本号信息,是代码宏定义,可以在代码中修改。
date
data命令是用于查询和设置当前系统时间,一般系统连上网络以后会定期调用ntp,来和服务器同步时间,这个命令可以查询同步时间和设置系统时间
> date
TZ(08):Tue Aug 11 18:03:14 2020 1597168994
UTC:Tue Aug 11 10:03:14 2020 1597140194
date -s "2001-01-01 12:13:14"
> date -s "2020-08-11 18:15:38"
set date to: 2020-08-11 18:15:38
TZ(08):Wed Aug 12 02:15:38 2020 1597198538
UTC:Tue Aug 11 18:15:38 2020 1597169738
date -s "2001-01-01 12:13:14"
log
log命令可以用于控制打印等级和打印的模块
> log
Usage:
set level: log level 0~5
0:disable 1:F 2:E 3:W 4:I 5:D
add ignore tag: log ignore tag
clear ignore tag: log ignore
> log level 0
> log ignore fota
log tag ignore list:
fota
> log ignore RTC
log tag ignore list:
fota
RTC
>
log level num 用于控制打印等级
0:关闭日志打印;
1:打印F级别的日志;
2:打印E级别及以上的日志;
3:打印W级别及以上的日志;
4:打印I级别及以上的日志;
5:打印D级别及以上的日志,也是就日志全开
log ignore tag 用于控制各个模块的打印
例如log ignore RTC 表示关闭 RTC 模块的日志打印
需要注意的是:log 命令只能控制通过 LOG 模块打印出来的日志,直接通过 printf 接口打印的日志 不能被拦截。所以推荐用 LOG 模块去打印日志。
kv
kv是一个小型的存储系统,通过key-value 的方式存储在flash中
> kv
Usage:
kv set key value
kv get key
kv setint key value
kv getint key
kv del key
>
kv set key value 是设置字符串类型的value kv setint key value 是设置整形的value
例如:
kv set wifi_ssid my_ssid
kv set wifi_psk my_psk
如上两条命令是用于设置wifi的 ssid和psk,重启后系统会去通过kv接口获取flash的kv value值,从而进行联网。
ifconfig
> ifconfig
wifi0 Link encap:WiFi HWaddr 18:bc:5a:60:d6:04
inet addr:192.168.43.167
GWaddr:192.168.43.1
Mask:255.255.255.0
DNS SERVER 0: 192.168.43.1
WiFi Connected to b0:e2:35:c0:c0:ac (on wifi0)
SSID: yocdemo
channel: 11
signal: -58 dBm
ifconfig命令可以查看当前 网络连接的状态,其中:
• 第一部分是 本机的网络状态,包括本机mac地址,本机IP,网关地址、掩码、DNS Server地址
• 第二部分是 连接的路由器信息,包括wifi的名称,mac地址,连接的信道、信号质量
1.2 创建自己的串口命令
上一节介绍了系统内置的串口命令,本节介绍如何创建自定义串口命令用于调试。 YoC中,串口命令代码模块为cli,其代码头文件为cli.h。自定义串口命令时,需要包含这个头文件。
代码示例如下:
/*
* Copyright (C) 2019-2020 Alibaba Group Holding Limited
*/
#include <string.h>
#include <aos/cli.h>
#define HELP_INFO \
"Usage:\n\tmycmd test\n"
static void cmd_mycmd_ctrl_func(char *wbuf, int wbuf_len, int argc, char **argv)
{
int i;
for (i = 0; i < argc; i ++) {
printf("argv %d: %s\n", i, argv[i]);
}
printf(HELP_INFO);
}
void cli_reg_cmd_my_cmd(void)
{
static const struct cli_command cmd_info = {
"my_cmd",
"my_cmd test",
cmd_mycmd_ctrl_func,
};
aos_cli_register_command(&cmd_info);
}
其中,
• 需要定义一个被cli回调的函数,当串口输入这个命令时就会触发这个回调,本例为cmd_mycmd_ctrl_func;
• 需要定义一个命令字符串,用于cli比较用于输入字符串来触发回调,本例为my_cmd;
• 需要定义帮助信息,用于串口输入help命令时打印出来,本例为my_cmd test;
• 最后在系统初始化时把这个命令注册到cli里面,本例为cli_reg_cmd_my_cmd;
这样就可以拥有自己的串口调试命令了,效果如下:
> my_cmd first cmd test
argv 0: my_cmd
argv 1: first
argv 2: cmd
argv 3: test
Usage:
mycmd test
2. 使用GDB调试
GDB是C/C++ 程序员的程序调试利器,很多问题使用GDB调试都可以大大提高效率。GDB在查看变量、跟踪函数跳转流程、查看内存内容、查看线程栈等方面都非常方便。
同时,GDB也是深入理解程序运行细节最有效的方式之一,GDB 对于学习了解C语言代码、全局变量、栈、堆等内存区域的分布都有一定的帮助。
下面我们来介绍GDB在基于玄铁内核的嵌入式芯片上的调试方法。
2.1 建立GDB连接
这一小节讲解一些嵌入式GDB调试使用的基础知识,和在PC上直接使用GDB调试PC上的程序会有一些区别。
CK GDB是运行在PC上的GDB程序,通过仿真器和JTAG协议与开发板相连接,可以调试基于玄铁CPU内核的芯片。其中DebugServer为作为连接GDB和CKLink仿真器的桥梁和翻译官,一端通过网络与GDB连接,另一端通过USB线与仿真器连接。
由于GDB与DebugServer通过网络通讯,他们可运行在同一个或不同的PC上。仿真器CKLink与开发板通过20PIN的JTAG排线连接。
CKLink
CKLink 实物如下图所示。可以通过淘宝购买 。其使用方法可以查看:CKLink设备使用指南。
DebugServer
DebugServer有Windows 版本和Linux版本,下载和安装过程请参考:《Windows调试环境安装》,《Linux调试环境安装》。
以Windows版本的DebugServer为例,安装完成以后,打开程序有如下界面:
点击连接按钮,如果连接成功会有CPU和GDB的信息打印,告知当前连接的CPU信息和开启的GDB服务信息。具体使用可以参考OCC资源下载页面下的文档:《DebugServer User Guide_v5.10》。
2.2 启动GDB及配置
GDB工具包含在整体的编译调试工具链里面,也可以通过OCC下载。GDB的使用都需要通过命令行完成,通过在终端敲入命令来完成交互 启动GDB通过如下命令进行:
csky-abiv2-elf-gdb xxx.elf
其中 xxx.elf 为当前板子上运行的程序,它包含了所有的程序调试信息,如果缺少elf文件则无法进行调试。
启动GDB后输入如下命令连接DebugServer。这条命令在DebugServer的界面会有打印,可以直接复制。
target remote [ip]:[port]
需要注意的是:运行GDB程序对应的PC需要能够通过网络访问DebugServer开启的对应的IP
连上以后就可以通过GDB 访问调试开发板上的芯片了。
.gdbinit 文件
.gdbinit 文件为GDB启动时默认运行的脚本文件,我们可以在.gdbinit 文件里面添加启动默认需要执行的命令,例如:target remote [ip]:[port],那么在启动GDB的时候,会直接连接DebugServer,提高调试效率。
2.3 常用GDB命令
这一小节介绍一些常用的GDB命令及使用方法。
加载程序
• 命令全名: load
• 简化 :lo
• 说明 :将 elf 文件 加载到 芯片中,这个命令对代码在flash运行的芯片无效。
举例:
(cskygdb) lo
Loading section .text, size 0x291a00 lma 0x18600000
section progress: 100.0%, total progress: 69.01%
Loading section .ram.code, size 0x228 lma 0x18891a00
section progress: 100.0%, total progress: 69.02%
Loading section .gcc_except_table, size 0x8f8 lma 0x18891c28
section progress: 100.0%, total progress: 69.08%
Loading section .rodata, size 0xeeac4 lma 0x18892520
section progress: 100.0%, total progress: 94.12%
Loading section .FSymTab, size 0x9c lma 0x18980fe4
section progress: 100.0%, total progress: 94.13%
Loading section .data, size 0x2e3c4 lma 0x18981400
section progress: 100.0%, total progress: 98.98%
Loading section ._itcm_code, size 0x9b70 lma 0x189af7c4
section progress: 100.0%, total progress: 100.00%
Start address 0x18600014, load size 3903412
Transfer rate: 238 KB/sec, 4003 bytes/write.
继续执行
• 命令全名:continue
• 简化 :c
• 说明 :继续执行被调试程序,直至下一个断点或程序结束。
举例:
(cskygdb)c
当DebugServer连接上开发板,程序会自动停止运行。等GDB挂进去以后,用c就可以继续运行程序。
当程序在运行的时候,GDB直接挂入也会使程序停止运行,同样用c 命令可以继续运行程序。
同样,当 load完成后,也可以使用c运行程序。
暂停运行
使用组件按键 ctrl + c 可以停止正在运行的程序。
停止运行程序后就可以进行各种命令操作,如打印变量,打断点,查看栈信息,查看内存等。
当操作完成以后,使用c 继续运行,或者使用 n/s 单步执行调试。
打印变量
• 命令全名: print
• 简化 : p
打印变量可以打印各种形式
• 变量
• 变量地址
• 变量内容
• 函数
• 计算公式
举例:
(cskygdb)p g_tick_count
(cskygdb)p &g_tick_count
(cskygdb)p *g_tick_count
(cskygdb)p main
(cskygdb)p 3 * 5
可以指定打印格式 按照特定格式打印变量
• x 按十六进制格式显示变量。
• d 按十进制格式显示变量。
• o 按八进制格式显示变量。
• t 按二进制格式显示变量。
• c 按字符格式显示变量。
通过这个功能,还可以进行简单的 各种进制转换
举例:
(cskygdb)p /x g_tick_count
(cskygdb)p /x 1000
(cskygdb)p /t 1000
注意:有些局部变量会被编译器优化掉,可能无法查看。 p 命令是万能的,可以 p 变量地址,可以p 变量内容,可以p 函数地址;基本上所有符号,都可以通过p查看内容。
设置断点
• 命令全名: breakpoint
• 简化 :b
设置断电可以让程序自动停止在你希望停止的地方,断点可以以下面多种方式设置
• 行号
• 函数名
• 文件名:行号
• 汇编地址
举例:
(cskygdb)b 88
(cskygdb)b main
(cskygdb)b main.c:88
(cskygdb)b *0x18600010
硬件断点
嵌入式芯片一般都有硬件断点可以设置,它相对于普通断点的不同是,该断点信息保存在cpu 调试寄存器里面,由cpu通过运行时的比较来实现断点功能,而普通断点则是通过修改该处代码的内容,替换成特定的汇编代码来实现断点功能的。 需要注意的是:硬件断点的设置会影响cpu的运行速度,但是对于一些微型的嵌入式芯片,代码放在flash这种无法写入,只能读取介质上时,就只能通过设置硬件断点才能实现断点功能,普通的断点设置将不会生效。 设置硬件断点通过另外一个命令设置,举例:
(cskygdb)hb main
设置内存断点
• 命令全名: watchpoint
• 简化 :watch
设置内存断电可以在内存的内容发生变化的时候 自动停止运行。可以通过设置变量、内存断点
举例:
(cskygdb)watch g_tick_count
(cskygdb)watch *0x18600010
内存断点和硬件断点是相同的原理,只要是cpu运行导致的内存修改都会自动停止运行。内存断点和硬件断点都会都会占用cpu的调试断点数,每个芯片都由固定有限的个数可供设置,一般为4个或者8个等。
查看断点
• 命令全名:info breakpoint
• 简化 :i b
举例:
(cskygdb) i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x18704f9c in main
at vendor/tg6100n/aos/aos.c:110
2 breakpoint keep y 0x1871ca9c in cpu_pwr_node_init_static
at kernel/kernel/pwrmgmt/cpu_pwr_hal_lib.c:88
使能断点
• 命令全名:enable
• 简化 :en
举例:
(cskygdb)en 1
禁止断点
• 命令全名:disable
• 简化 :dis
举例:
(cskygdb)dis 1
查看栈信息
• 命令全名: backtrace
• 简化 : bt
例如:
(cskygdb) bt
#0 board_cpu_c_state_set (cpuCState=1, master=1)
at vendor/tg6100n/board/pwrmgmt_hal/board_cpu_pwr.c:103
#1 0x1871cb98 in cpu_pwr_c_state_set_ (
all_cores_need_sync=<optimized out>, master=<optimized out>,
cpu_c_state=CPU_CSTATE_C1,
p_cpu_node=0x189d2100 <cpu_pwr_node_core_0>)
at kernel/kernel/pwrmgmt/cpu_pwr_hal_lib.c:275
#2 _cpu_pwr_c_state_set (target_c_state=CPU_CSTATE_C1)
at kernel/kernel/pwrmgmt/cpu_pwr_hal_lib.c:495
#3 cpu_pwr_c_state_set (target_c_state=CPU_CSTATE_C1)
at kernel/kernel/pwrmgmt/cpu_pwr_hal_lib.c:524
#4 0x1871d20c in tickless_enter ()
at kernel/kernel/pwrmgmt/cpu_tickless.c:381
#5 0x1871ce74 in cpu_pwr_down ()
at kernel/kernel/pwrmgmt/cpu_pwr_lib.c:70
#6 0x187095a4 in idle_task (arg=<optimized out>)
at kernel/kernel/rhino/k_idle.c:48
#7 0x1870bf44 in krhino_task_info_get (task=<optimized out>,
idx=<optimized out>, info=0x8000000)
at kernel/kernel/rhino/k_task.c:1081
Backtrace stopped: frame did not save the PC
选择栈帧
• 命令全名: frame
• 简化 :f
举例:
(cskygdb) f 2
#2 _cpu_pwr_c_state_set (target_c_state=CPU_CSTATE_C1)
at kernel/kernel/pwrmgmt/cpu_pwr_hal_lib.c:495
495 ret = cpu_pwr_c_state_set_(p_cpu_node, target_c_state, master, FALSE);
选择了栈帧就可以通过 p 命令查看该栈函数内的局部变量了。(函数内的局部变量是存放在栈空间中的)
单步执行
• 命令全名: next
• 简化 :n
举例:
(cskygdb) n
单步执行进入函数
• 命令全名: step
• 简化 :s
举例:
(cskygdb) s
单步执行(汇编)
• 命令全名: nexti
• 简化 :ni
举例:
(cskygdb) ni
单步执行进入函数(汇编)
• 命令全名: stepi
• 简化 :si
举例:
(cskygdb) si
相对于s 的单步执行,si的单步执行精确到了汇编级别,每一个命令执行一条汇编指令。对于优化比较严重的函数,s 的按行 单步执行 流程往往会比较混乱,按汇编的单步执行则会比较符合芯片底层的逻辑。当然使用si单步调试程序,也需要程序员对于汇编指令有比较好的了解,调试难度也比较大。但是对于嵌入式程序,编译器必然会对程序进行各种优化,s 的单步调试往往不是很好的选择。
完成当前函数
• 命令全名: finish
• 简化 :fin
举例:
(cskygdb) fin
当想跳出该函数调试时,使用该命令会相当方便。但是该命令有一个限制,当在不会支持普通断点的设备上调试时(代码放在flash上执行),这个命令需要配合 另一条命令才能生效
(cskygdb) set debug-in-rom
这条命令的意思是,告诉gdb这个代码是放在flash上的,需要使用硬件断点才能使用fin命令,这条命令只需要执行一次。
设置变量
• 命令格式:
set [variable] = [value]
举例:
(cskygdb) set g_tick_count = 100
(cskygdb) set *0x186000010 = 0x10
在调试一些程序逻辑时,通过设置变量数值可以让程序走期望的流程,来方便调试。
查看内存
• 命令格式
x /[n][f][u] [address]
其中:
• n 表示显示内存长度,默认值为1
• f 表示显示格式,如同上面打印变量定义
• u 表示每次读取的字节数,默认是4bytes
– b 表示单字节
– h 表示双字节
– w 表示四字节
– g 表示八字节
举例:
(cskygdb) x /20x 0x18950000
0x18950000: 0x6f445f6c 0x72652077 0x21726f72 0x6c43000a
0x18950010: 0x546b636f 0x72656d69 0x5f6c633a 0x61746164
0x18950020: 0x6c633e2d 0x6365535f 0x74696220 0x2070616d
0x18950030: 0x61207369 0x30206c6c 0x21212120 0x6c43000a
0x18950040: 0x546b636f 0x72656d69 0x5f6c633a 0x61746164
这条命令对于调试踩内存,栈溢出等大量内存变化的场景非常有帮助。
2.4 快速上手调试
接下来,你可以找一块开发板,按照下面步骤体验GDB调试过程:
• 如前面介绍,下载并安装DebugServer
• GDB 连上DebugServer
• lo //灌入编译好的 elf
• b main //打断点到 main函数入口
• c //运行程序
• 如果顺利,这时程序应该自动停在main函数入口
• n //单步执行下一行程序,可以多执行几次
• 找几个全局变量, p 查看结果
大部分开发板上电都自动会运行程序,连上DegbuServer就会停止运行。
注意事项
• 调试的时候 elf 文件 一定要和运行程序对应上,不然没法调试,使用一个错误的elf文件调试程序,会出现各种乱七八糟的现象。而且同一份代码,不同的编译器,不同的主机编译出来的elf都可能不相同。所以保存好编译出来的elf相当重要
• 对于一些代码运行在 flash的芯片方案,GDB调试的时候要注意转换,和在ram上GDB调试命令有一些不一样。
• watch 只能观察到CPU的内存更改行为,如果是外设(DMA等)运行导致的内存变化,不能被watch到
• CKLink 连接开发板可能存在各种问题连接不上,要仔细检查,包括:开发板是否上电,芯片是否上电,芯片是否在运行,JTAG排线是否插反等等。
3. CPU异常分析及调试
3.1 CPU异常案例
在开发板运行过程中,有时会突然出现如下打印,进而程序停止运行,开发板也没有任何响应:
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
这段打印表明程序已经崩溃。接下来以它为例,来一步一步分析如何调试和解决。
3.2 基础知识介绍
3.2.1 关键寄存器说明
• pc:程序计数器,它是一个地址指针,指向了程序执行到的位置
• sp:栈指针,它是一个地址指针,指向了当前任务的栈顶部,它的下面存了这个任务的函数调用顺序和这些被调用函数里面的局部变量。在玄铁CPU框架里,它对应了 R14 寄存器
• lr:连接寄存器,它也是一个地址指针,指向子程序返回地址,也就是说当前程序执行返回后,执行的第一个指令就是lr寄存器指向的指令,在玄铁CPU框架里,它对对应了 R15 寄存器
• epc:异常保留程序计数器,它是一个地址指针,指向了异常时的程序位置,这个寄存器比较重要,出现异常后,我们就需要通过这个寄存器来恢复出现异常时候的程序位置。
• epsr:异常保留处理器状态寄存器,它是一个状态寄存器,保存了出异常前的系统状态。
这几个重要的寄存器都在上面的异常打印中打印出来了。
3.2.2 关键文件说明
• 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
3.2.3 异常号说明
在XT CPU架构里,不同的cpu异常会有不同的异常号,我们往往需要通过异常号来判断可能出现的问题。
这些异常中,出现最多的是 1、2 号异常,4、7 偶尔也会被触发,3号异常比较好确认。
3.3 异常分析过程
GDB准备及连接
参考上节:《2. 使用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,r14,r15 三个寄存器是找回现场的关键寄存器,其中r14,r15分别是 sp 寄存器和 lr寄存器,pc寄存器需要设置成epc。其余的通用寄存器是一些函数传参和函数内的局部变量。
设置完成以后,通过 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=<optimized out>, idx=<optimized out>, 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了。
3.4 通过异常号找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)
请注意你代码里的 memset、memcpy、malloc、free 、strcpy等调用。
大部分2号异常和1号异常的问题,异常的时候都不是第一现场了,也就是说异常点之前就已经出问题了。
比如之前就出现了 memcpy的 内存访问越界,内存拷贝超出变量区域了。memcpy的时候是不会异常的,只有当程序使用了这些被memcpy 踩了内存时,才会出现一号或二号异常。
这个时候异常点已经不是那个坑的地方了,属于“前人埋坑,后人遭殃”型问题。
如果是一些很快就复现的问题,我们可以通过GDB watch命令,watch那些被踩的内存或变量来快速的定位是哪段代码踩了内存。
如果是一些压测出现的问题,压测了2天,出了一个2号异常,恭喜你,碰到大坑了。类似这种,比较难复现的问题,watch已经不现实了。
结合异常现场GDB查看变量、内存信息和review代码逻辑,倒推出内存踩踏点,是比较正确的途径。
再有,就是在可疑的代码中加 log日志,增加压测的机器,构造缩短复现时间的case等一些技巧来加快BUG解决的速度。
CPU Exception: NO.4/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,除零就会出现 三号异常。
3.5 不用GDB找到异常点
有些时候无法使用GDB去查看异常点,或者搭环境不是很方便怎么办? 这个时候我们可以通过反汇编文件和epc地址来查看产生异常的函数。 打开yoc.asm 反汇编文件,在文件内搜索epc地址,就可以找到对应的函数,只是找不到对应的行号。
例如:
186d9b14 <board_yoc_init>:
186d9b14: 14d3 push r4-r6, r15
186d9b16: 1430 subi r14, r14, 64
186d9b18: e3ffffc6 bsr 0x186d9aa4 // 186d9aa4 <speaker_init>
186d9b1c: 3001 movi r0, 1
186d9b1e: e3fe3221 bsr 0x1869ff60 // 1869ff60 <av_ao_diff_enable>
186d9b22: e3fe4ca9 bsr 0x186a3474 // 186a3474 <booab_init>
186d9b26: e3fffe7d bsr 0x186d9820 // 186d9820 <firmware_init>
...
186d9bfc: 1010 lrw r0, 0x188d1a50 // 186d9c3c <board_yoc_init+0x128>
186d9bfe: e00c6aeb bsr 0x188671d4 // 188671d4 <printf>
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 <board_yoc_init+0x46>
186d9c14: 188d19c0 .long 0x188d19c0
如上的汇编代码,根据异常的epc地址0x186d9c12,我们可以确认异常发生在board_yoc_init函数内。