2016年8月30日 星期二

GDB實用教學:自動化你的debug

這是一篇看了"Give me 15 minutes & I'll change your view of GDB"的心得以及延伸,原始投影片可以見CppCon2015

如果不曉得什麼是GDB,建議從「Linux 除錯利器 - GDB 簡介」開始學習,此篇是給知道什麼是GDB,但總覺得GDB不是那麼好用的開發者,重新認識GDB,跟隨著影片的腳步,重現一次。



GDB也有GUI

雖然GDB是在終端機中使用,輸入l可以列出目前的程式碼,但大家一定還是覺得很難用,所以就有了tui的出現:

$ gdb -tui
or
$ gdbtui

都可以用指令的方式進入GDB的圖形介面。
但如果只以單純的
$ gdb
進入的話,只要按下ctrl + x +a 就可以了。


這邊以一個簡單的helloworld程式(hello.c)來說明:


#include <stdio.h>

int main(void)
{
  int i = 0;
  printf("Hello, world\n");
  printf("i is %d\n",i);
  i++;
  printf("i is now %d\n",i);                                                                                                                                     
}



接下來在終端機輸入: 


$ gcc -g -O0 -o hello hello.c
$ gdb ./hello

(gdb) start 



然後按下ctrl + x +a 後,就變成這樣了!

這時候,如果有遇到破圖,按下ctrl + L 就可以刷新畫面。
如要關閉tui,在按一次ctrl + x +a 即可

到這裡,大部份已知有tui可以用的人,都覺得沒什麼!
不過驚喜的就是:如果按ctrl + x + 2會出現組合語言(類似objdump出來的結果)的視窗!
再按一次相同的組合鍵,會跳出register的內容!
而且是以循環的方式呈現。
於gdbtui中,沒辦法使用 方向鍵上、方向鍵下 來使用之前使用過的指令,記得改用ctrl + p 和 ctrl +n來使用歷史指令。
或是透過ctrl + x + o 來切換active window,方向鍵上下,可能就會有你想要的功能了!



GDB也有python可以用

先來看個小示範,一樣是上面的hello.c
(gdb) python
> print('hello python')


此時按下ctrl+D(或輸入end) 退出python,就可以看到輸出!



那更進一步呢:


GDB的物件可以與python互通:


試著輸入以下的指令,會有神奇的事情發生!
(gdb) python gdb.Breakpoint('7')
(gdb) python bp = gdb.Breakpoint('10')
(gdb) python bp.enabled = False
(gdb) python print (gdb.breakpoints())
(gdb) python print (gdb.breakpoints()[0].location)
(gdb) python print (gdb.breakpoints()[0].enabled)
(gdb) python python_var = gdb.parse_and_eval('code_var_name')
(gdb) python gdb.execute('next')
(gdb) python help('gdb')

這個功能,可以使GDB來輸出某些structure,使其格式漂漂亮亮。
更多功能可以參看這裡,20:47處有範例

在01:12:00之後,有更多的python與GDB的相關教學!



透過GDB自動化除錯

如果開發過程式的朋友都知道,有些memory/stack的問題,並非每次都會發生,但發生了又不知道問題在哪,這時候就是GDB出場救援的時刻了!

這裡的範例程式碼,可以參閱這裡(就上面那影片),約24:48處有快速瀏覽的過程。 細節的部份,請參考影片,這裡僅以文字敘述:


$ while ./bug_program ; do echo OK; done

不斷測試,之後會出現segmentation fault(core dump)

輸入


$ ls -lth core*

會出現某個紀錄最後stack的檔案
透過gdb -c 可以進行post mortem debugging,
原則上就是這個檔案會存有,當時該process的address space以及registers的值(包含general registers 和fault status registers)
這與我大四時,曾進行過的某個專案有點關係,有興趣可以看看。


那麼該如何透過gdb利用這個core dump file呢?


$ gdb -c [core-dump-file]

會發現,即使輸入bt,因為stack已經被破壞了,GDB目前無法辨識。
所以只好利用自動化的GDB測試,重現問題點,並且透過reverse-gdb重現。



$ gdb ./bug_program


(gdb) b main
(gdb) b [program last line]
或
(gdb) b _exit

GDB會分別顯示對應的中斷點編號,或是透過


(gdb) info b

可以看到對應編號,然後透過command + 編號,可以指定當中斷發生時,要做的對應行為:(假設main是中斷點2),每一行就是一個指令,然後結束輸入end即可。


(gdb) command 2
(gdb) record
(gdb) continue
(gdb) end

如果之後過程中,有要一直按enter顯示下一頁,可以透過
(gdb) set pagination off
來避免這個麻煩。

然後如果程式的最後一行假設是中斷點3:
(gdb) command 3
(gdb) run
(gdb) end
可以透過這些指令,讓程式不斷的在GDB中重複運行,接下來就可以準備開始了!


(gdb) run
就會不斷重複執行,直到問題發生!


問題發生後

(gdb) p $pc
可以看到pc的位置,輸出可能是:1 = (void (*))0x5e4c5d00
如果透過:
(gdb) p/x *0x5e4c5d00

可能會出現
Cannot access memory at address 0x5e4c5d00
因為一般core dump就是因為存取到不合法的address(不屬於此address space的空間)所導致的。
如果輸入
(gdb) bt
應該也只會得到一堆"?"的frame,無用的資訊。


這時候reverse GDB就要出場了

輸入
(gdb) reverse-stepi
就能看到程式最後執行到哪一行了!
那倒底怎麼回事了呢?
透過
(gdb) disassemble
可以發現,是最後的return發生問題,此時輸入:

(gdb) print $sp
可能會輸出:2 = (void *) 0x7fffffffdc98
(gdb) print *(long**)0x7fffffffdc98
這就是有bug的address !!(sp企圖存取的位置)

那這個位置是誰導致的問題呢?
透過watch point來觀察:
(gdb) watch *(long**)0x7fffffffdc98
(gdb) reverse-continue

watch-point就會噴出到底是哪一行code搞的鬼!


作者的"~./gdbinit"設定分享

set history save on                                                                                                                   
set print pretty on
set pagination on
set confirm off

GDB debug "fork"

常用指令:
set follow-fork-mode child|parent
set detach-on-fork off
info inferiors
inferior [infno]
set follow-exec-mode new|same
add inferior [name]
clone-inferior

GDB debug "threads"

thread apply 1-4 print $sp
thread apply all backtrace
thread apply all backtrace full


GDB catch points

manual


延伸指令

投影片的最後有列出一些好用的指令,這裏特別列出,我覺得很有用的部分:
(gdb) info line hello.c:6
(gdb) info line *$pc
列出該行程式碼,對應的組合語言



(gdb) save breakpoints breakpoints.record
將使用過的breakpoints儲存成breakpoints.record(檔案)



(gdb) dprintf location,template,expression [,expression ...]
將中斷點設置在特定位置(function/line)後,並且輸出對應的expression(到指定的位置)
範例:
(gdb) set dprintf-channel mylog
(gdb) set dprintf-function fprintf
(gdb) set dprintf-style call
(gdb) dprintf main,"main function encounterd!\n"
Dprintf 1 at 0x40052e: file hello.c, line 5.
(gdb) info breakpoints 
Num     Type           Disp Enb Address            What
1       dprintf        keep y   0x000000000040052e in main at hello.c:5
        call (void) fprintf (mylog,"main function encounterd!\n")
參考這裡  或 manual



(gdb) break xxx if yyy
這個值得注意的是,GDB裡面可以呼叫程式內已有的function或一些標準的library function,如這個stackoverflow的例子



(gdb) call [function]
類似(gdb) print [function],可是不會顯示回傳值,但經過測試,兩者行為幾乎一樣



(gdb) commands
所有中斷點,發生時,要做的指令清單,輸入end結束。
(gdb) commands
(gdb) [some commands]
(gdb) silent
(gdb) [some commands]
(gdb) end
silent是特殊的中斷點指令,會使silent之後的GDB提示都不顯示,如果commands後馬上接silent,連中斷發生的提示都不會顯示!



另外,GDB一樣有類似macro的機制可以使用,用來看大量structure還不錯用,可以參考我以前用過簡單的macro

1 則留言:

  1. 你好:

    請問有memory/stack的問題程式碼可以提供嗎? 想照著操作gdb 看 pc & sp 跟利用 watch 找到你所提到的情況
    謝謝

    回覆刪除