Linux GDB 調試

1  調試信息和調試原理

  通常要調試某個程序,爲了能清晰地看到調試的每一行代碼、調用的堆棧信息、變量名和函數名等信息,須要調試程序含有調試符號信息。使用 gcc 編譯程序時,若是加上 -g 選項便可在編譯後的程序中保留調試符號信息。如下命令將生成一個帶調試信息的程序 hello_world。redis

gdb -g -o hello_world hello_world.c

固然咱們能夠經過gdb來判斷程序是否帶有調試信息:數組

gdb hello_world 

若是gdb 加載成功之後,會顯示以下信息:sass

Reading symbols from /root/testclient/hello_server...donesession

咱們也可使用 Linux 的 strip 命令移除掉某個程序中的調試信息。多線程

strip hello_world

調試時建議關閉編譯器的程序優化選項,由於程序優化後調試顯示的代碼和實際代碼可能就會有差別了,這會給排查問題帶來困難。函數

gdb -g -O0 hello_world world_world.c

  


 

2  啓動GDB調試

2.1  直接調試目標程序

gdb hello_world   // gdb + 程序名

2.2  附加進程

當一個程序已經啓動,咱們想調試這個程序,但又不想重啓這個程序時,能夠經過使用 gdb attach 進程ID 來將gdb調試器附加到想要調試的程序上。優化

gdb attach 進程ID

當⽤ gdb attach 上⽬標進程後,調試器會暫停下來,此時可使⽤ continue 命令讓程序繼續運⾏,或者加上相應的斷點再繼續運⾏程序。當調試完程序想結束這次調試時,⽽且不對當前進程有任何影響,能夠在 GDB 的命令⾏界⾯輸⼊ detach 命令 讓程序與 GDB 調試器分離。ui

(gdb)  detach

2.3  調試core文件

Linux 系統默認是不開啓程序崩潰產⽣ core ⽂件這⼀機制的,咱們可使⽤ ulimit -c 命令來查看系統是否開啓了這⼀機制。使用ulimit -c unlimited 直接將core文件的大小修改爲不限制大小。而後就能夠經過如下命令調試core文件:spa

gdb filename corename

經過調試core文件能夠看到程序崩潰的地方,使用bt命令查看崩潰時的調用堆棧,進一步分析找到崩潰的緣由。當有多個程序崩潰時,有時很難經過core文件的名稱來判斷對應的core文件。咱們能夠本身修改core文件的名稱來解決該問題。經過修改/proc/sys/kernel/core_uses_pid 能夠控制產生的 core 文件的文件名,修改方式以下:命令行

echo "/corefile/core-%e-%p-%t" > /proc/sys/kernel/core_pattern

 文件名各個參數的說明以下:

參數名稱 參數含義(中文)
%p 添加 pid 到 core 文件名中
%u 添加當前 uid 到 core 文件名中
%g 添加當前 gid 到 core 文件名中
%s 添加致使產生 core 的信號到 core 文件名中
%t 添加 core 文件生成時間(UNIX)到 core 文件名中
%h 添加主機名到 core 文件名中
%e 添加程序名到 core 文件名中

假設如今的程序叫 test,咱們設置該程序崩潰時的 core 文件名以下:

echo "/root/testcore/core-%e-%p-%t" > /proc/sys/kernel/core_pattern 

那麼最終會在 /root/testcore/ 目錄下生成的 test 的 core 文件名格式以下:

-rw-------. 1 root root 409600 Jan 14 13:54 core-test-13154-1547445291  

 

3  GDB經常使用調試命令

命令名稱 命令縮寫 命令說明
run r 運行一個程序
continue c 讓暫停的程序繼續運行
next n 運行到下一行
step s 若是有調用函數,進入調用的函數內部,至關於 step into
until u 運行到指定行停下來
finish fi 結束當前調用函數,到上一層函數調用處
return return 結束當前調用函數並返回指定值,到上一層函數調用處
jump j 將當前程序執行流跳轉到指定行或地址
print p 打印變量或寄存器值
backtrace bt 查看當前線程的調用堆棧
frame f 切換到當前調用線程的指定堆棧,具體堆棧經過堆棧序號指定
thread thread 切換到指定線程
break b 添加斷點
tbreak tb 添加臨時斷點
delete del 刪除斷點
enable enable 啓用某個斷點
disable disable 禁用某個斷點
watch watch 監視某一個變量或內存地址的值是否發生變化
list l 顯示源碼
info info 查看斷點 / 線程等信息
ptype ptype 查看變量類型
disassemble dis 查看彙編代碼
set args   設置程序啓動命令行參數
show args   查看設置的命令行參數

 

3.1  run 命令

前面說的 gdb filename 命令只是附加的一個調試文件,並無啓動這個程序,須要輸⼊ run 命令(簡寫爲 r)啓動這個程序。

 

3.2  continue 命令

當 GDB 觸發斷點或者使⽤ Ctrl + C 命令中斷下來後,想讓程序繼續運⾏,只要輸⼊ continue 命令便可(簡寫爲 c)。

 

3.3  break 命令

break 命令(簡寫爲 b)即咱們添加斷點的命令,可使⽤如下⽅式添加斷點:

  • break functionname,在函數名爲 functionname 的⼊⼝處添加⼀個斷點;
  • break LineNo,在當前⽂件⾏號爲 LineNo 處添加⼀個斷點;
  • break filename:LineNo,在 filename ⽂件⾏號爲 LineNo 處添加⼀個斷點。

3.4  backtrace 與 frame 命令

backtrace 命令(簡寫爲 bt)⽤來查看當前調⽤堆棧。查看調用的堆棧信息後可使⽤ frame + 堆棧編號 命令(簡寫爲 f),切換⾄指定堆棧頂部。

 

3.5  info break、 enable、 disable 和 delete 命令

在程序中加了不少斷點,⽽咱們想查看加了哪些斷點時,可使⽤ info break 命令(簡寫爲 info b)。

(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000423450 in main at server.c:3709 breakpoint already hit 1 time
2 breakpoint keep y 0x000000000049c1f0 in _redisContextConnectTcp at net.c:267

  由上面的內容片斷能夠知道,目前一共增長了2個斷點,斷點1觸發1次,斷點2未觸發過。咱們想禁⽤某個斷點時,使⽤「 disable 斷點編號 」就能夠禁⽤這個斷點了,同理,被禁⽤的斷點也可使⽤「 enable 斷點編號 」從新啓⽤。使⽤「delete 編號」能夠刪除某個斷點,若是輸⼊ delete 不加命令號,則表示刪除全部斷點。

 

3.6  list命令

  第⼀次輸⼊ list 命令會顯示斷點處先後的代碼,繼續輸⼊ list 指令會以遞增⾏號的形式繼續顯示剩下的代碼⾏,⼀直到⽂件結束爲⽌。固然 list 指令還能夠往前和日後顯示代碼,命令分別是「list + (加號) 」和「list - (減號) 」。

 

3.7  print 和 ptype 命令

  經過 print + 變量名 能夠打印出指定變量的值,print 命令也能夠顯示進⾏⼀定運算的表達式計算結果值,甚⾄能夠顯示⼀些函數的執⾏結果值。舉個例子,咱們可使用 p a+b+c 來打印這三個變量的結果值;也可使用 p func() 命令輸出一個可執行函數 func() 的執行結果。

  print 命令不只能夠輸出表達式結果,同時也能夠修改變量的值,咱們嘗試將端⼝號從 6379 改爲 6400 試試:

(gdb) p server.port=6400
$24 = 6400
(gdb) p server.port
$25 = 6400
(gdb)

  ptype 命令,其含義是「print type」,就是輸出⼀個變量的類型。

 

3.8  info thread 和 info args命令

⽤ info thread命令來查看當前進程有哪些線程,分別中斷在何處。

(gdb) info thread
Id Target Id Frame
4 Thread 0x7fffef7fd700 (LWP 53065) "redis-server" 0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
3 Thread 0x7fffefffe700 (LWP 53064) "redis-server" 0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
2 Thread 0x7ffff07ff700 (LWP 53063) "redis-server" 0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
* 1 Thread 0x7ffff7fec780 (LWP 53062) "redis-server" 0x00007ffff73ee923 in epoll_wait () from /lib64/libc.so.6

   經過 info thread 的輸出能夠知道 redis-server 正常啓動後,⼀共產⽣了 4 個線程,包括⼀個主線程和三個⼯做線程,線程編號(Id 那⼀列)分別是 四、 三、 二、 1。三個⼯做線程(二、 三、 4)分別阻塞在 Linux API pthread_cond_wait 處,⽽主線程(1)阻塞在 epoll_wait 處。當有多個線程時,咱們可使用 backtrace 命令查看調用堆棧,經過過堆棧判斷 GDB 做用在哪一個線程上面。如何切換到其餘線程呢?能夠經過「thread 線程編號」切換到具體的線程上去。例如,想切換到線程 2 上去,只要輸⼊ thread 2 便可。

   info 命令還能夠⽤來查看當前函數的參數值,組合命令是 info args

 

3.9  next、 step、 util、 finish 和 return 命令

  next 命令(簡寫爲n)是讓 GDB 調到下⼀條命令去執⾏,這⾥的下⼀條命令不⼀定是代碼的下⼀⾏,⽽是根據程序邏輯跳轉到相應的位置。這⾥有⼀個⼩技巧,在 GDB 命令⾏界⾯若是直接按下回⻋鍵,默認是將最近⼀條命令從新執⾏⼀遍,所以,當使⽤ next 命令單步調試時,沒必要反覆輸⼊ n 命令,直接回⻋就能夠了。

  step 命令(簡寫爲 s)就是「單步步⼊」(step into),顧名思義,就是遇到函數調⽤,進⼊函數內部。

  finish 命令會執⾏函數到正常退出該函數;⽽ return 命令是⽴即結束執⾏當前函數並返回,也就是說,若是當前函數還有剩餘的代碼未執⾏完畢,也不會執⾏了。

  until 命令(簡寫爲 u)能夠指定程序運⾏到某⼀⾏停下來。好比直接輸入 u 1888,就能夠快速執行完中間的內容,直接跳到1888行。固然也可使用斷點的方式,可是使用until命令會更便捷。

 

3.10  set args 和 show args 命令

  不少程序須要咱們傳遞命令⾏參數。在 GDB 調試中,不少⼈會以爲可使⽤ gdb filename args 這種形式來給 GDB 調試的程序傳遞命令⾏參數,這樣是不⾏的。正確的作法是在⽤ GDB 附加程序後,在使⽤ run 命令以前,使⽤「 set args 參數內容 」來設置命令⾏參數

  若是單個命令⾏參數之間含有空格,可使⽤引號將參數包裹起來。

(gdb) set args "999 xx" "hu jj"
(gdb) show args
Argument list to give program being debugged when it is started is ""999 xx" "hu j
j"".
(gdb) 

  若是想清除掉已經設置好的命令⾏參數,使⽤ set args 不加任何參數便可。

(gdb) set args
(gdb) show args
Argument list to give program being debugged when it is started is "".
(gdb)

 

3.11  tbreak 命令

  tbreak 命令也是添加⼀個斷點,第⼀個字⺟「t」的意思是 temporarily(臨時的),也就是說這個命令加的斷點是臨時的,所謂臨時斷點,就是⼀旦該斷點觸發⼀次後就會⾃動刪除。添加斷點的⽅法與上⾯介紹的 break命令⼀模⼀樣,這⾥再也不贅述。

 

3.12  watch 命令

  watch 命令是⼀個強⼤的命令,它能夠⽤來監視⼀個變量或者⼀段內存,當這個變量或者該內存處的值發⽣變化時, GDB 就會中斷下來。被監視的某個變量或者某個內存地址會產⽣⼀個 watch point(觀察點)。

 

3.13  display 命令

  display 命令監視的變量或者內存地址,每次程序中斷下來都會⾃動輸出這些變量或內存的值。例如,假設程序有⼀些全局變量,每次斷點停下來我都但願 GDB 能夠⾃動輸出這些變量的最新值,那麼使⽤「 display變量名 」設置便可。

 


 

4  GDB 調試技巧

4.1  將 print 打印結果顯示完整

  當使⽤ print 命令打印⼀個字符串或者字符數組時,若是該字符串太⻓, print 命令默認顯示不全的,咱們能夠經過在 GDB 中輸⼊ set print element 0 命令設置⼀下,這樣再次使⽤ print 命令就能完整地顯示該變量的全部字符串了。

 

4.2  讓被 GDB 調試的程序接收信號

void prog_exit(int signo)
{
    std::cout << "program recv signal [" << signo << "] to exit." << std::endl;
}
int main(int argc, char* argv[])
{
    //設置信號處理
    signal(SIGCHLD, SIG_DFL);
    signal(SIGPIPE, SIG_IGN);
    signal(SIGINT, prog_exit);
    signal(SIGTERM, prog_exit);
    int ch;
    bool bdaemon = false;
    while ((ch = getopt(argc, argv, "d")) != -1)
  {
    switch (ch)
    {
      case 'd':
      bdaemon = true;
      break;
    }
  }
  if (bdaemon)
  daemon_run();
  //省略⽆關代碼...
}

  在這個程序中,咱們接收到 Ctrl + C 信號(對應信號 SIGINT)時會簡單打印⼀⾏信息,⽽當⽤ GDB 調試這個程序時,因爲 Ctrl + C 默認會被 GDB 接收到(讓調試器中斷下來),致使⽆法模擬程序接收這⼀信號。解決這個問題有兩種⽅式:在 GDB 中使⽤ signal 函數⼿動給程序發送信號,這⾥就是 signal SIGINT;改變 GDB 信號處理的設置,經過 handle SIGINT nostop print 告訴 GDB 在接收到 SIGINT 時不要停⽌,並把該信號傳遞給調試⽬標程序 。

(gdb) handle SIGINT nostop print pass
SIGINT is used by the debugger.
Are you sure you want to change it? (y or n) y
Signal Stop Print Pass to program Description
SIGINT No Yes Yes Interrupt
(gdb)

 

4.3  多線程下禁⽌線程切換

假設如今有 5 個線程,除了主線程,⼯做線程都是下⾯這樣的⼀個函數:

void thread_proc(void* arg)
{
  //代碼⾏1
  //代碼⾏2
  //代碼⾏3
  //代碼⾏4
  //代碼⾏5
  //代碼⾏6
  //代碼⾏7
  //代碼⾏8
  //代碼⾏9
  //代碼⾏10
  //代碼⾏11
  //代碼⾏12
  //代碼⾏13
  //代碼⾏14
  //代碼⾏15
}

  爲了能說清楚這個問題,咱們把四個⼯做線程分別叫作 A、 B、 C、 D。假設 GDB 當前正在處於線程 A 的代碼⾏ 3 處,此時輸⼊ next 命令,咱們指望的是調試器跳到代碼⾏ 4 處;或者使⽤「u 代碼⾏10」,那麼咱們指望輸⼊ u 命令後調試器能夠跳轉到代碼⾏ 10 處。可是在實際狀況下, GDB 可能會跳轉到代碼⾏ 1 或者代碼⾏ 2 處,甚⾄代碼⾏ 1三、代碼⾏ 14 這樣的地⽅也是有可能的,這不是調試器 bug,這是多線程程序的特色,當咱們從代碼⾏ 4 處讓程序 continue 時,線程A 雖然會繼續往下執⾏,可是若是此時系統的線程調度將 CPU 時間⽚切換到線程 B、 C 或者 D 呢?那麼程序最終停下來的時候,處於代碼⾏ 1 或者代碼⾏ 2 或者其餘地⽅就不奇怪了,⽽此時打印相關的變量值,可能就不是咱們須要的線程 A 的相關值。

   爲了解決調試多線程程序時出現的這種問題, GDB 提供了⼀個在調試時將程序執⾏流鎖定在當前調試線程的命令: set scheduler-locking on。固然也能夠關閉這⼀選項,使⽤ set scheduler-locking off。

 

4.4  條件斷點

所謂條件斷點,就是滿⾜某個條件纔會觸發的斷點,這⾥先舉⼀個直觀的例⼦

void do_something_func(int i)
{
  i ++;
  i = 100 * i;
}
int main()
{
  for(int i = 0; i < 10000; ++i)
  {
    do_something_func(i);
  }
  return 0;
}

  在上述代碼中,假如咱們但願當變量 i=5000 時,進⼊ do_something_func() 函數追蹤⼀下這個函數的執⾏細節。添加條件斷點的命令是 break [lineNo] if [condition],其中 lineNo 是程序觸發斷點後須要停下的位置, condition 是斷點觸發的條件。這⾥能夠寫成 break 11 if i==5000,其中, 11 就是調⽤ do_something_fun() 函數所在的⾏號。固然這⾥的⾏號必須是合理⾏號,若是⾏號⾮法或者⾏號位置不合理也不會觸發這個斷點。

 

4.5  使⽤ GDB 調試多進程程序

  在實際的應⽤中,若有這樣⼀類程序,如 Nginx,對於客戶端的鏈接是採⽤多進程模型,當 Nginx 接受客戶端鏈接後,建立⼀個新的進程來處理這⼀路鏈接上的信息來往,新產⽣的進程與原進程互爲⽗⼦關係,那麼如何⽤ GDB 調試這樣的⽗⼦進程呢?⼀般有兩種⽅法:⽤ GDB 先調試⽗進程,等⼦進程 fork 出來後,使⽤ gdb attach 到⼦進程上去,固然這須要從新開啓⼀個 session 窗⼝⽤於調試, gdb attach 的⽤法在前⾯已經介紹過了;GDB 調試器提供了⼀個選項叫 follow-fork,可使⽤ show follow-fork mode 查看當前值,也能夠經過set follow-fork mode 來設置是當⼀個進程 fork 出新的⼦進程時, GDB 是繼續調試⽗進程仍是⼦進程取值是 child),默認是⽗進程( 取值是 parent)。

相關文章
相關標籤/搜索