上一章: 智能語音元件适配指南 | 《無需從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函數内。