開發與維運

智能語音組件適配指南 | 《無需從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 命令,可以查看當前所有支持命令:

image.png
image.png
image.png

ps 命令可以打印出當前系統所有的線程狀態,每項含義介紹如下:
image.png

部分信息詳細說明如下:

• 線程狀態有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

image.png

具體顯示的信息如下:

其中 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排線連接。

image.png

CKLink

CKLink 實物如下圖所示。可以通過淘寶購買 。其使用方法可以查看:CKLink設備使用指南

image.png

DebugServer

DebugServer有Windows 版本和Linux版本,下載和安裝過程請參考:《Windows調試環境安裝》,《Linux調試環境安裝》。

以Windows版本的DebugServer為例,安裝完成以後,打開程序有如下界面:

image.png

點擊連接按鈕,如果連接成功會有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異常會有不同的異常號,我們往往需要通過異常號來判斷可能出現的問題。

image.png
image.png

這些異常中,出現最多的是 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函數內。

Leave a Reply

Your email address will not be published. Required fields are marked *