通常要調試某個程序,爲了能清晰地看到調試的每一行代碼、調用的堆棧信息、變量名和函數名等信息,須要調試程序含有調試符號信息。使用 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...done
session
咱們也可使用 Linux 的 strip 命令移除掉某個程序中的調試信息。多線程
strip hello_world
調試時建議關閉編譯器的程序優化選項,由於程序優化後調試顯示的代碼和實際代碼可能就會有差別了,這會給排查問題帶來困難。函數
gdb -g -O0 hello_world world_world.c
gdb hello_world // gdb + 程序名
當一個程序已經啓動,咱們想調試這個程序,但又不想重啓這個程序時,能夠經過使用 gdb attach 進程ID 來將gdb調試器附加到想要調試的程序上。優化
gdb attach 進程ID
當⽤ gdb attach 上⽬標進程後,調試器會暫停下來,此時可使⽤ continue 命令讓程序繼續運⾏,或者加上相應的斷點再繼續運⾏程序。當調試完程序想結束這次調試時,⽽且不對當前進程有任何影響,能夠在 GDB 的命令⾏界⾯輸⼊ detach 命令 讓程序與 GDB 調試器分離。ui
(gdb) detach
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
命令名稱 | 命令縮寫 | 命令說明 |
---|---|---|
run | r | 運行一個程序 |
continue | c | 讓暫停的程序繼續運行 |
next | n | 運行到下一行 |
step | s | 若是有調用函數,進入調用的函數內部,至關於 step into |
until | u | 運行到指定行停下來 |
finish | fi | 結束當前調用函數,到上一層函數調用處 |
return | return | 結束當前調用函數並返回指定值,到上一層函數調用處 |
jump | j | 將當前程序執行流跳轉到指定行或地址 |
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 | 查看設置的命令行參數 |
前面說的 gdb filename 命令只是附加的一個調試文件,並無啓動這個程序,須要輸⼊ run 命令(簡寫爲 r)啓動這個程序。
當 GDB 觸發斷點或者使⽤ Ctrl + C 命令中斷下來後,想讓程序繼續運⾏,只要輸⼊ continue 命令便可(簡寫爲 c)。
break 命令(簡寫爲 b)即咱們添加斷點的命令,可使⽤如下⽅式添加斷點:
backtrace 命令(簡寫爲 bt)⽤來查看當前調⽤堆棧。查看調用的堆棧信息後可使⽤ frame + 堆棧編號 命令(簡寫爲 f),切換⾄指定堆棧頂部。
在程序中加了不少斷點,⽽咱們想查看加了哪些斷點時,可使⽤ 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 不加命令號,則表示刪除全部斷點。
第⼀次輸⼊ list 命令會顯示斷點處先後的代碼,繼續輸⼊ list 指令會以遞增⾏號的形式繼續顯示剩下的代碼⾏,⼀直到⽂件結束爲⽌。固然 list 指令還能夠往前和日後顯示代碼,命令分別是「list + (加號) 」和「list - (減號) 」。
經過 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」,就是輸出⼀個變量的類型。
⽤ 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。
next 命令(簡寫爲n)是讓 GDB 調到下⼀條命令去執⾏,這⾥的下⼀條命令不⼀定是代碼的下⼀⾏,⽽是根據程序邏輯跳轉到相應的位置。這⾥有⼀個⼩技巧,在 GDB 命令⾏界⾯若是直接按下回⻋鍵,默認是將最近⼀條命令從新執⾏⼀遍,所以,當使⽤ next 命令單步調試時,沒必要反覆輸⼊ n 命令,直接回⻋就能夠了。
step 命令(簡寫爲 s)就是「單步步⼊」(step into),顧名思義,就是遇到函數調⽤,進⼊函數內部。
finish 命令會執⾏函數到正常退出該函數;⽽ return 命令是⽴即結束執⾏當前函數並返回,也就是說,若是當前函數還有剩餘的代碼未執⾏完畢,也不會執⾏了。
until 命令(簡寫爲 u)能夠指定程序運⾏到某⼀⾏停下來。好比直接輸入 u 1888,就能夠快速執行完中間的內容,直接跳到1888行。固然也可使用斷點的方式,可是使用until命令會更便捷。
不少程序須要咱們傳遞命令⾏參數。在 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)
tbreak 命令也是添加⼀個斷點,第⼀個字⺟「t」的意思是 temporarily(臨時的),也就是說這個命令加的斷點是臨時的,所謂臨時斷點,就是⼀旦該斷點觸發⼀次後就會⾃動刪除。添加斷點的⽅法與上⾯介紹的 break命令⼀模⼀樣,這⾥再也不贅述。
watch 命令是⼀個強⼤的命令,它能夠⽤來監視⼀個變量或者⼀段內存,當這個變量或者該內存處的值發⽣變化時, GDB 就會中斷下來。被監視的某個變量或者某個內存地址會產⽣⼀個 watch point(觀察點)。
display 命令監視的變量或者內存地址,每次程序中斷下來都會⾃動輸出這些變量或內存的值。例如,假設程序有⼀些全局變量,每次斷點停下來我都但願 GDB 能夠⾃動輸出這些變量的最新值,那麼使⽤「 display變量名 」設置便可。
當使⽤ print 命令打印⼀個字符串或者字符數組時,若是該字符串太⻓, print 命令默認顯示不全的,咱們能夠經過在 GDB 中輸⼊ set print element 0 命令設置⼀下,這樣再次使⽤ print 命令就能完整地顯示該變量的全部字符串了。
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)
假設如今有 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。
所謂條件斷點,就是滿⾜某個條件纔會觸發的斷點,這⾥先舉⼀個直觀的例⼦
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() 函數所在的⾏號。固然這⾥的⾏號必須是合理⾏號,若是⾏號⾮法或者⾏號位置不合理也不會觸發這個斷點。
在實際的應⽤中,若有這樣⼀類程序,如 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)。