天天看點

gdb調試詳解與darknet架構gdb調試過程

準備工作

開啟core, 采集程式崩潰的狀态

首先你跟着我做開啟core崩潰狀态采集. 可以通過

ulimit -c

檢視,如果是

表示沒有開啟. 開啟按照下面操作:

sudo gedit /etc/profile
           

/etc/profile

最後一行添加下面幾句話設定全局開啟 core檔案調試,大小不限.

# No core files by default 0, unlimited is oo
ulimit -S -c unlimited > /dev/null 2>&1
           

最後立即生效.

source /etc/profile
           

再跟着我做, 因為生成的

core

檔案同名會覆寫. 這裡為其加上一個

core

命名規則, 讓其變成

[core.pid]

格式.

sudo gedit /etc/sysctl.conf
           

在該檔案的最後的加上如下幾句話,并儲存

# open, add core.pid
kernel.core_pattern = ./core_%t_%p_%e
kernel.core_uses_pid = 1
           

立即啟用

sudo sysctl -p /etc/sysctl.conf
           

最後是

ulimit -c

cat /proc/sys/kernel/core_uses_pid

檢視,下面狀态表示core啟用都搞好了.

gdb調試詳解與darknet架構gdb調試過程
如果顯示沒有開啟成功,可以試試登出系統或者重新開機

簡單接觸 GDB , 開始調試 r n p

第一個示範代碼

heoo.c

#include <stdio.h>

int g_var = 0;

static int _add(int a, int b) {
    printf("_add callad, a:%d, b:%d\n", a, b);
    return a+b;
}

int main(void) {
    int n = 1;
   
    printf("one n=%d, g_var=%d\n", n, g_var);
    ++n;
    --n;
   
    g_var += 20;
    g_var -= 10;
    n = _add(1, g_var);
    printf("two n=%d, g_var=%d\n", n, g_var);
   
    return 0;
}
           

我們從下圖說起,

使用指令

gcc -g -Wall -o heoo.out heoo.c
gdb heoo.out
           

gdb heoo.out

表示

gdb

加載

heoo.out

開始調試. 如果需要使用

gdb

調試的話編譯的時候

gcc

需要加上

-g

指令.

其中

l

指令表示 檢視加載源碼内容. .

gdb調試詳解與darknet架構gdb調試過程

下面将示範如何加斷點,使用指令

b 函數名

或者

b 行數

r

表示調試的程式開始運作.

gdb調試詳解與darknet架構gdb調試過程

p

指令表示 列印值.

n

表示過程調試, 到下一步. 不管子過程如何都不進入. 直接一次跳過.

gdb調試詳解與darknet架構gdb調試過程

下面的s 表示單步調試, 遇到子函數,會進入函數内部調試.

gdb調試詳解與darknet架構gdb調試過程

總結一下 .

l

檢視源碼 ,

b

加斷點,

r

開始運作調試,

n

下一步,

s

下一步但是會進入子函數.

p

輸出資料.

c

跳過直到下一個斷點處,

watch 變量名

給變量添加監視點,

whatis 變量名

列印變量名的類型,

finish

跳出目前代碼(之前跳入調試),

q

表示程式退出.

到這裡gdb 基本會用了. 是不是也很容易. 直白. 小代碼可以随便調試了.

看到這裡基礎知識普及完畢了. 後面可以不看了. 有機會再看. 好那我們接着扯.

gdb其它開發中用的指令

開始扯一點, linux總是敲指令操作, 也很不安全. 有時候暈了. 寫這樣編譯指令.

gcc -g -Wall -o heoo.c heoo.out
           

非常恐怖,

heoo.c

代碼删除了.

heoo.out => heoo.c

先建立後生成失敗退出. 原先的内容被抹掉了. 哈哈. 伺服器開發, 經驗不足, 熟練度不夠.自己都怕自己.

gdb 其它常用指令用法 c q b info

首先看 用到的調試檔案

houge.c

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

/*
 * arr 隻能是數組
 * 傳回目前數組長度
 */
#define LEN(arr) (sizeof(arr)/sizeof(*arr))

// 簡單數組列印函數
static void _parrs(int a[], int len) {
    int i = -1;
    puts("目前數組内容值如下:");

    while(++i < len)
        printf("%d ", a[i]);   
    putchar('\n');
}

// 簡單包裝宏, arr必須是數組
#define PARRS(arr) \
    _parrs(arr, LEN(arr))

#define _INT_OLD (23)

/*
 * 主函數,簡單測試
 * 測試 core檔案,
 * 測試 宏調試
 * 測試 堆棧記憶體資訊
 */
int main(void) {
    int i;
    int a[_INT_OLD];
    int* ptr = NULL;   

    // 來個随機數填充值吧
    srand((unsigned)time(NULL));
    for(i=0; i<LEN(a); ++i)
        a[i] = rand()%222;
   
    PARRS(a);

    //全員加double, 包含一個錯誤友善測試
    for(i=1; i<=LEN(a); ++i)
        a[i] <<= 1;
    PARRS(a);

    // 為了錯,強制錯
    *ptr = 0;

    return 0;
}
           

同樣需要仔細看下面圖中使用的指令. 首先對前言部分加深一些. 看下面

gdb調試詳解與darknet架構gdb調試過程

這個圖是前言的補充,

c

跳過直到下一個斷點處,

q

表示程式退出.

houge.c

中我們開始調試. 輸入下面指令進行運作:

gcc -g -Wall -o houge.out houge.c
./houge.out
           

一運作段錯誤, 出現了我們的

core.pid

檔案

gdb調試詳解與darknet架構gdb調試過程

通過

gdb houge.out core.27047

開始調試. 馬上定位出來了錯誤原因.

調試記憶體堆棧資訊

gdb調試詳解與darknet架構gdb調試過程

剛開始

print a

, 在

main

中當做數組處理.列印的資訊多. 後面在

_add

函數中,

a

就是個形參數組位址.

主要看

info args

檢視目前函數參數值

info locals

看目前函數棧上值資訊,

info registers

表示檢視寄存器值.

後面檢視記憶體資訊 需要記得東西多一些. 先看圖,

x /23dw a

意思是 檢視 從

a

位址開始 23個 4位元組 有符号十進制數 輸出.

gdb調試詳解與darknet架構gdb調試過程

關于

x

更加詳細見下面,這個指令常用于監測記憶體變化.調試中特别常用.

用gdb檢視記憶體格式:
    x /nfu ptr

說明
x 是 examine 的縮寫
n表示要顯示的記憶體單元的個數

f表示顯示方式, 可取如下值
x 按十六進制格式顯示變量。
d 按十進制格式顯示變量。
u 按十進制格式顯示無符号整型。
o 按八進制格式顯示變量。
t 按二進制格式顯示變量。
a 按十六進制格式顯示變量。
i 指令位址格式
c 按字元格式顯示變量。
f 按浮點數格式顯示變量。

u表示一個位址單元的長度
b表示單位元組,
h表示雙位元組,
w表示四位元組,
g表示八位元組

Format letters are o(octal), x(hex), d(decimal), u(unsigned decimal),
t(binary), f(float), a(address), i(instruction), c(char) and s(string).
Size letters are b(byte), h(halfword), w(word), g(giant, 8 bytes)

ptr 表示從那個位址開始
           

gdb設定條件斷點

如下如所示,很簡單

b 17 if i == 8

. 在17行設定一個斷點,并且隻有

i==8

的時候才會觸發.

gdb調試詳解與darknet架構gdb調試過程

gdb删除斷點

  • d

    後面跟斷點索引1,2,3…
  • clear

    行數或名稱. 删除哪一行斷點. 看下面示範
gdb調試詳解與darknet架構gdb調試過程

到這裡 介紹的gdb調試技巧基本都夠用了. 感覺用圖形ide,例如vs調試也就用到這些了.

估計gdb調試突破20min過去了.夠用了. 後面可以不用看了.

gdb調試回退

加入你正在使用GDB7.0以上版本的調試器并且運作在支援反向調試的平台,你就可以用以下幾條指令來調試程式:

reverse-continue
           

反向運作程式知道遇到一個能使程式中斷的事件(比如斷點,觀察點,異常)。

reverse-step
           

反向運作程式到上一次被執行的源代碼行。

reverse-stepi
           

反向運作程式到上一條機器指令

reverse-next
           

反向運作到上一次被執行的源代碼行,但是不進入函數。

reverse-nexti
           

反向運作到上一條機器指令,除非這條指令用來傳回一個函數調用、整個函數将會被反向執行。

reverse-finish
           

反向運作程式回到調用目前函數的地方。

set exec-direction [forward | reverse]
           

設定程式運作方向,可以用平常的指令

step

continue

等來執行反向的調試指令。

上面的反向運作也可以了解為撤銷後面運作的語句所産生的效果,回到以前的狀态。

好的,接下來我們來試試看如何反向調試。

首先确認自己的平台支援程序記錄回放(Process Record and Replay),當在調試器啟用程序記錄回放功能時,調試器會記錄下子程序,也就是被調試程序的每一步的運作狀态與上一步運作狀态的差異,需要撤銷的時候就可以很友善回到上一步。

假設我們有以下C程式:

int main(int argc, const char *argv[])  
{  
  int a = 0;  
  a = 1;  
  a = 2;  
  return 0;  
} 
           

将它編譯并加上調試符号:

gcc -Wall -g a.c 
           

開始調試

gdb a.out 
           

接下來設定一個斷點在第三行:

(gdb) b 3  
Breakpoint 1 at 0x804839a: file a.c, line 3. 
           

運作,程式會在第三行的地方停下來:

(gdb) r  
Starting program: /home/cheryl/a.out   
Breakpoint 1, main (argc=1, argv=0xbffff3e4) at a.c:3  
3 int a = 0;  
           

給變量a設定監視點友善我們觀察:

(gdb) watch a  
Hardware watchpoint 2: a  
           

啟動程序記錄回放:

(gdb) record  
           

現在每運作一步調試器都會記錄下變化,以便回溯。我們連續執行3條語句。

(gdb) n  
4 a = 1;  
(gdb)   
Hardware watchpoint 2: a  
Old value = 0  
New value = 1  
main (argc=1, argv=0xbffff3e4) at a.c:5  
5 a = 2;  
(gdb)   
Hardware watchpoint 2: a  
Old value = 1  
New value = 2  
main (argc=1, argv=0xbffff3e4) at a.c:6  
6 return 0;  
           

可以看到,a的值先是從0變為了1,然後變為2,如果想讓程式倒退回到以前的狀态怎麼辦?可以用reverse-next指令:

(gdb) reverse-next  
Hardware watchpoint 2: a  
Old value = 2  
New value = 1  
main (argc=1, argv=0xbffff3e4) at a.c:5  
5 a = 2;  
(gdb)   
Hardware watchpoint 2: a  
Old value = 1  
New value = 0  
main (argc=1, argv=0xbffff3e4) at a.c:4  
4 a = 1;  
(gdb)   
No more reverse-execution history.  
main (argc=1, argv=0xbffff3e4) at a.c:3  
3 int a = 0;  
(gdb)   
           

這樣程式就倒退到了我們啟動程序記錄回放的地方,a的值經過兩步回到了最初的狀态。

若需要關閉程序記錄回放,可以使用record stop:

(gdb) record stop  
Process record is stoped and all execution log is deleted. 
           

GDB 格式化結構體輸出

set print address on

打開位址輸出,當程式顯示函數資訊時,GDB會顯出函數的參數位址。系統預設為打開的,

show print address

檢視目前位址顯示選項是否打開。

set print array on

打開數組顯示,打開後當數組顯示時,每個元素占一行,如果不打開的話,每個元素則以逗号分隔。這個選項預設是關閉的。與之相關的兩個指令如下,我就不再多說了。

set print array off

show print array

set print elements

這個選項主要是設定數組的,如果你的數組太大了,那麼就可以指定一個來指定資料顯示的最大長度,當到達這個長度時,GDB就不再往下顯示了。如果設定為0,則表示不限制。

show print elements

檢視print elements的選項資訊。

set print null-stop

如果打開了這個選項,那麼當顯示字元串時,遇到結束符則停止顯示。這個選項預設為off。

set print pretty on

如果打開printf pretty這個選項,那麼當GDB顯示結構體時會比較漂亮。

set print pretty off

show print pretty

set print union on

set print union off

show print union

列印 C 中的聯合體。預設是 on 。

gdb 列印數組

可以用下面的方法來顯示數組

p *[email protected]
           

其中

p

相當于

print

array

就是數組首位址,也可以是數組名,

len

是想要顯示的數組的長度。

比如我有一個數組的定義

int a[] = {1, 2, 3, 4, 5};
           

那麼想要顯示的時候就可以寫:

p *[email protected]
           

這樣就會顯示數組a中的所有元素。

也可以使用

display

在每一步調試的時候都顯示:

display *[email protected]
           

取消顯示就用

undisplay

,不過這時候要寫顯示的号碼。

gdb 調試darknet實際工程

darknet

源代碼是makefile管理的,之前不會在Linux調試大型項目,今天探索了一下,這裡介紹一下。

準備工作

從這裡下載下傳源代碼

修改makefile檔案中

DEBUG=0

改為

DEBUG=1

進行調試。其中編譯選項

-O0

,意思是不進行編譯優化,gdb在預設情況下會使用

-O2

,會出現print變量中出現

<optimized out>

接着編譯源代碼:

make clean
make
           

根目錄會出現

darknet

可執行檔案。

在工程根目錄運作如下指令下載下傳權重:

wget https://pjreddie.com/media/files/yolov3-tiny.weights
           

開始調試

終端輸入如下語句,開始調試

gdb ./darknet
           

gdb

指令中輸入運作程式需要的參數類型

set args detect cfg/yolov3-tiny.cfg yolov3-tiny.weights data/dog.jpg
           

為了對整個工程進行調試,這裡需要将

src

目錄添加進來,在

gdb

指令中輸入如下指令:

DIR ./src
           

gdb

指令中為

main

函數設定斷點

b main
           

開始調試,在

gdb

指令中輸入

r

,回車,發現程式停留在第一行。

接着可以在第435行,即

char *outfile = find_char_arg(argc, argv, "-out", 0);

,打上斷點

b 435

gdb

指令中輸入

b parser.c:761

在子函數

parser.c

的761行打上斷點;

輸入

c

,回車,程式跳到下一個斷點,即停留下一個斷點所在行;

輸入

n

單步執行,不跳入子函數。

輸入

s

指令單步執行并跳入此處調用的子函數;

輸入

print 變量名

或者

p 變量名

即可檢視該變量值;輸入

finish

跳出子函數;

輸入

q

結束調試。

gdb 多線程多程序調試

到這裡實戰中用的機會少了, 也就老鳥會用上些. 這部分可以調試,不好調試. 一般一調估計小半天就走了. 好,那我們處理最後10min.

gdb調試宏

gdb調試詳解與darknet架構gdb調試過程

首先看上面指令

  • macro expand 宏(參數) => 得到宏導出内容.
  • info macro 宏名 => 宏定義内容

如果你需要用到上面gdb功能, 檢視和導出宏的話.還需要gcc 支援,生成的時候加上 -ggdb3如下

gcc -Wall -ggdb3 -o houge.out houge.c
           

就可以使用了. 擴充一下 對于 gcc 編譯的有個過程叫做 預編譯

gcc -E -o *.i *.c.

這時候處理多數宏,直接展開, 也可以檢視最後結果. 也算也是一個黑科技.

開始多線程調試

首先看測試用例

dasheng.c

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

// 聲明一個都用的量
static int _old;

// 線程跑的函數
static void* _run(void* arg) {
    int piyo = 10;   
    int n = *(int*)arg;
    int i;
   
    //設定線程分離
    pthread_detach(pthread_self());
   
    for(i=0; i<n; ++i) {
        printf("n=%d, i=%d\n", n, i);
        ++_old;
        printf("n=%d, piyo = %d, _old=%d\n", n, piyo, _old);
    }

    return NULL;
}

#define _INT_PTX (3)

int main(void) {
    int i, rt, j;
    pthread_t tx[_INT_PTX];

    puts("main beign");   

    for(i=0; i<_INT_PTX; ++i) {
        // &i 是有問題的, 但是這裡為了測試, 可以亂搞
        rt = pthread_create(tx+i, NULL, _run, &i);
        if(rt < 0) {
            printf("pthread_create create error! rt = %d, i=%d\n", rt, i);
            break;
        }
    }

    //CPU忙等待
    for(j=0; j<1000000000; ++j)
        ;       
    puts("end");   

    return 0;
}
           

編譯指令

gcc -Wall -g -o dasheng.out dasheng.c -lpthread
           

那先看下面測試圖

gdb調試詳解與darknet架構gdb調試過程

上面

info threads

檢視所有運作的線程資訊.

*

表示目前調試的線程.

後面

l _run

表示檢視

_run

附近代碼. 當然還有

l 16

檢視16行附近檔案内容.

gdb多線程切換 測試如下

gdb調試詳解與darknet架構gdb調試過程

thread 3表示切換到第三個線程, info threads 第一列id 就是 thread 切換的id.

上面測試線程 就算你切換到 thread 3. 其它線程還是在跑的. 我們用下面指令 隻讓待調試的線程跑. 其它線程阻塞.

gdb調試詳解與darknet架構gdb調試過程

set scheduler-locking on

開始多線程單獨調試. 不用了 設定

set scheduler-locking off

關閉. 又會回到你調試這個, 其它線程不阻塞.

gdb調試詳解與darknet架構gdb調試過程

總結 多線程調試常用就這三個實用指令

  • info threads
  • thread id
  • set scheduler-locking on/off

分别是檢視,切換,設定同步調試.到這裡多線程調試基本完畢了.

開始gdb多進行調試

首先看

liaobude.c

測試代碼

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

// 聲明一個都用的量
static int _old;

// 線程跑的函數
static void _run(int n) {
    int piyo = 10;   
    int i;
   
    ++n;   
    for(i=0; i<n; ++i) {
        printf("n=%d, i=%d\n", n, i);
        ++_old;
        printf("n=%d, piyo = %d, _old=%d\n", n, piyo, _old);
    }
}

#define _INT_PTX (3)

int main(void) {
    int i;
    pid_t rt;

    puts("main beign");   

    for(i=0; i<_INT_PTX; ++i) {
        // &i 是有問題的, 但是這裡為了測試, 可以亂搞
        rt = fork();
        if(rt < 0) {
            printf("fork clone error! rt = %d, i=%d\n", rt, i);
            break;
        }
        if(rt == 0) {
            _run(i);
            exit(EXIT_FAILURE);
        }
    }

    //等待子程序結束
  for(;;) {
    rt = waitpid(-1, NULL, WNOHANG);
    if(rt>=0 || errno==EINTR)
      continue;
    break;
  }  
   
    puts("end");   

    // 這裡繼續等待
    for(i=0; i<190; ++i){
        printf("等待 有緣人[%d]!\n", i);
        sleep(1);
    }   

    return 0;
}
           

編譯指令

gcc -Wall -g -o liaobude.out liaobude.c
           

其實對多程序調試, 先介紹一個 常用的, 調試正在運作的程式. 首先讓

./liaobude.out

跑起來.

gdb調試詳解與darknet架構gdb調試過程

再通過

ps -ef

找到需要調試的程序. 複制程序檔案描述符pid.

這時候啟動gdb.

attach pid

gdb就把pid那個程序加載進來了. 加載的程序會阻塞到目前正在運作的地方. 直到使用指令控制. 這個功能還是非常猛的.

最後介紹 程序調試的有關指令(需要最新的gdb才會支援). 多程序的調試思路和多線程調試流程很相似.

GDB可以同時調試多個程式。
隻需要設定follow-fork-mode(預設值:parent)和detach-on-fork(預設值:on)即可。

   設定方法:set follow-fork-mode [parent|child]   set detach-on-fork [on|off]

   查詢正在調試的程序:info inferiors
   切換調試的程序: inferior <infer number>
           

具體的意思有

set follow-fork-mode [parent|child]   set detach-on-fork [on|off]

 parent                   on               隻調試主程序(gdb預設)
 child                      on               隻調試子程序
 parent                   off              同時調試兩個程序,gdb跟主程序,子程序block在fork位置
 child                      off              同時調試兩個程序,gdb跟子程序,主程序block在fork位置
           

更加詳細的 gdb 多程序調試demo 可以參照 http://blog.csdn.net/pbymw8iwm/article/details/7876797

使用方式和線程調試思路是一樣的. 就是gdb 的指令換了字元. 工作中多程序調試遇到少.

遇到了很少用gdb調試. 會用下面2種調試好辦法

  1. 寫單元測試
  2. 打日志檢測日志,分析

到這裡 gdb30分鐘内容講解完畢. 多試試寫寫練一練, gdb基本突破沒有問題.

參考連結

Linux基礎 30分鐘GDB調試快速突破

gdb調試4–回退

One more thing

更多關于人工智能、Python、C++、計算機等知識,歡迎通路我的個人部落格進行交流, 點這裡~~