第04課:GDB常用命令詳解(上)
本課的核心內容如下:
run命令
continue命令
break命令
backtrace與frame命令
info break、enable、disable和delete命令
list命令
print和ptype命令
為了結合實踐,這裡以除錯Redis原始碼為例來介紹沒一個命令,這裡先介紹一些常用命令的基礎用法,某些命令的高階用法會在後面講解。
Redis的最新原始碼下載地址可以在Redis官網(Redis中文網)獲得,使用wget命令將Redis原始碼檔案下載下來:
解壓:tar zxvf redis-5.0.3.tar.gz
進入生成的 redis-5.0.3 目錄使用makefile命令進行編譯。makefile命令是Linux程式編譯基本的命令,由於本課程的重點是Linux除錯,如果讀者不熟悉Linux編譯可以通過網際網路或相關書籍補充一下知識。
步驟:cd redis-5.0.3/
make -j 4
cd src
make test
然後啟動 redis-server
4.1 run命令
預設情況下,前面的課程中我們說gdb filename 命令知識個附加的一個除錯檔案,並沒有啟動這個程式,需要輸入run命令(簡寫為r)啟動這個程式。
(gdb) r Starting program: /home/wzq/Desktop/redis-5.0.3/src/redis-server [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". 21463:C 09 Jan 2019 14:33:20.281 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo 21463:C 09 Jan 2019 14:33:20.281 # Redis version=5.0.3, bits=64, commit=00000000, modified=0, pid=21463, just started 21463:C 09 Jan 2019 14:33:20.281 # Warning: no config file specified, using the default config. In order to specify a config file use /home/wzq/Desktop/redis-5.0.3/src/redis-server /path/to/redis.conf 21463:M 09 Jan 2019 14:33:20.282 * Increased maximum number of open files to 10032 (it was originally set to 1024). [New Thread 0x7ffff67ff700 (LWP 21467)] [New Thread 0x7ffff5ffe700 (LWP 21468)] [New Thread 0x7ffff57fd700 (LWP 21469)] _._ _.-``__ ''-._ _.-`` `. `_. ''-._ Redis 5.0.3 (00000000/0) 64 bit .-`` .-```. ```\/ _.,_ ''-._ ( ' , .-` | `, ) Running in standalone mode |`-._`-...-` __...-.``-._|'` _.-'| Port: 6379 | `-._ `._ / _.-' | PID: 21463 `-._ `-._ `-./ _.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | http://redis.io `-._ `-._`-.__.-'_.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | `-._ `-._`-.__.-'_.-' _.-' `-._ `-.__.-' _.-' `-._ _.-' `-.__.-' 21463:M 09 Jan 2019 14:33:20.283 # Server initialized 21463:M 09 Jan 2019 14:33:20.283 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect. 21463:M 09 Jan 2019 14:33:20.283 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled. 21463:M 09 Jan 2019 14:33:20.283 * Ready to accept connections
4.2 continue命令
當GDB觸發斷點或者使用Ctr+C命令中斷下來後,想讓程式繼續執行,只要輸入continue命令即可(簡寫為c)。當然,如果continue命令繼續觸發斷點,GDB就再次中斷下來。
4.3 break命令
break命令(簡寫為b)即我們新增斷點的命令,可以使用以下方式新增斷點:
break functionname,在函式名為functionname的入口處新增一個斷點。
break LineNo,在當前檔案行號為LineNo處新增一個斷點。
break filename:LineNo,在filename檔案行號為LineNo處新增一個斷點。
(gdb) b main Breakpoint 1 at 0x555555584cf0: file server.c, line 4003.
新增好之後,使用run命令重啟程式,就可以觸發這個斷點了,GDB就會停在斷點處。
(gdb) r The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/wzq/Desktop/redis-5.0.3/src/redis-server [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Breakpoint 1, main (argc=1, argv=0x7fffffffccc8) at server.c:4003 4003 int main(int argc, char **argv) { (gdb)
redis-server預設埠號是6379,我們知道這個埠號肯定是通過作業系統的socket API bind()函式建立的,通過檔案搜尋,我們找到呼叫這個函式的檔案,其位於anet.c441行。
我們使用break命令在這個地方加一個斷點:
(gdb) b anet.c:441 Breakpoint 2 at 0x55555558862b: file anet.c, line 441.
由於程式繫結埠號是redis-server啟動時初始化的,為了能觸發這個斷點,再次使用run命令啟動下這個程式,GDB第一次會觸發main()函式處的斷點,輸入continue命令繼續執行,接著觸發anet.c:441處的斷點:
(gdb) r The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/wzq/Desktop/redis-5.0.3/src/redis-server [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Breakpoint 1, main (argc=1, argv=0x7fffffffccc8) at server.c:4003 4003 int main(int argc, char **argv) { (gdb) c Continuing. 23268:C 09 Jan 2019 15:02:04.276 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo 23268:C 09 Jan 2019 15:02:04.276 # Redis version=5.0.3, bits=64, commit=00000000, modified=0, pid=23268, just started 23268:C 09 Jan 2019 15:02:04.276 # Warning: no config file specified, using the default config. In order to specify a config file use /home/wzq/Desktop/redis-5.0.3/src/redis-server /path/to/redis.conf 23268:M 09 Jan 2019 15:02:04.277 * Increased maximum number of open files to 10032 (it was originally set to 1024). Breakpoint 2, anetListen (err=0x5555559161e0 <server+576> "", s=6, sa=0x555555b2b240, len=28, backlog=511) at anet.c:441 441 if (bind(s,sa,len) == -1) { (gdb)
anet.c:441處的程式碼如下:
現在斷點停在第441行,所以當前檔案就是anet.c,可以直接使用“break 行號”新增斷點,例如,可以在第444行、450行、452行分別加一個斷點,看看這個函式執行完畢後走哪個return語句退出,則可以執行:
新增好這三個斷點後,我們使用continue命令繼續執行程式,發現程式執行到第452行中斷下來(即觸發Breakpoint 5):
說明redis-server繫結埠並設定偵聽(listen)成功,我們可以再開啟一個SSH視窗,驗證一下,發現6379埠確實已經處於偵聽狀態了:
[email protected]:~/Desktop/redis-5.0.3/src$ lsof -i -Pn | grep redis redis-ser 23268 wzq 6u IPv6 114334 0t0 TCP *:6379 (LISTEN)
4.4 backtrace 與 frame命令
backtrace命令(簡寫為bt)用來檢視當前呼叫堆疊。接上,redis-server現在中斷在anet.c:452行,可以通過backtrace命令來檢視當前的呼叫堆疊:
(gdb) bt #0 anetListen (err=0x5555559161e0 <server+576> "", s=6, sa=<optimized out>, len=<optimized out>, backlog=511) at anet.c:452 #1 0x00005555555887a4 in _anetTcpServer (err=0x5555559161e0 <server+576> "", port=<optimized out>, bindaddr=<optimized out>, af=10, backlog=511) at anet.c:487 #2 0x000055555558cf07 in listenToPort (port=6379, fds=0x55555591610c <server+364>, count=0x55555591614c <server+428>) at server.c:1924 #3 0x0000555555591ed0 in initServer () at server.c:2055 #4 0x0000555555585103 in main (argc=<optimized out>, argv=0x7fffffffccc8) at server.c:4160
這裡一共有5層堆疊,最頂層是main()函式,最底層是斷點所在的anetListen()函式,堆疊編號分別是#0-#4,如果想切換到其他堆疊處,可以使用frame命令(簡寫f),改命令的使用方法是“frame堆疊編號(編號不加#)”。在這裡一次切換至堆疊頂部,然後再切換回#0聯絡一下:
通過檢視上面的各堆疊,可以得出這裡的呼叫層級關係,即:
main()函式在4160行呼叫了initServer()函式
initServer()在第2055行呼叫了listenToPort()函式
listenToPort在1924行呼叫了anetTcp6Server()函式
_anetTcp6Server()在487行呼叫了anetListen()函式
當前斷點正好位於anetListen()函式中
4.5 info break、enable、disable、和delete命令
在程式中加了很多段點,而我們想檢視加了那些斷點時,可以使用info break命令(簡寫 info b)。
通過上面的內容可以知道,目前一共增加了5個斷點,其他資訊比如每個斷點的位置(所在的檔案和行號)、記憶體地址、斷點啟用和禁用狀態資訊一目瞭然。如果我們想禁用某個斷點使用“disable 斷點編號”既可以禁用這個斷點了,被禁用的斷點不會再被觸發;同理,被禁用的斷點也可以使用“enable 斷點編號”重新啟用。
使用disable 1以後,第一個斷點的End一欄的值由y變成n,重啟程式也不會再次觸發。
如果disable命令和enable命令不加斷點編號,則分別表示禁用和啟用所喲斷點。
使用“delete 編號”可以刪除某個斷點,如delete 2 3則表示刪除的斷點2和斷點3。
同樣道理,如果輸入delete不叫命令號,則表示刪除所有斷點。
4.6 list命令
list命令和後面介紹的print命令都是GDB除錯中用到的頻率最高的命令,list命令(簡寫為l)可以檢視當前斷點處的程式碼。使用frame命令切換到剛才的堆疊#3處,然後輸入list命令看下效果:
斷點停在2055行,輸入list命令以後,會顯示第2055行前後的10行程式碼,再次輸入list命令試一下:
程式碼繼續往後顯示10行,也就是說,第一次輸入list命令會顯示斷點處前後的程式碼,繼續輸入list指令會以遞增行號的形式繼續顯示剩下的程式碼行,一直到檔案結束為止。當然list指令還可以往前和往後顯示程式碼,命令分別是“list +”和“list -”;
list預設顯示多少行可以通過修改相關的GDB配置,由於我們一般不會修改這個預設顯示行數,這裡就不再浪費篇幅介紹了。list不僅可以顯示當前斷點處的程式碼,也可以顯示其他檔案某一行的程式碼,更多的用法可以在GDB中輸入help list檢視。
使用GDB的目的是除錯,因此更關心的是斷點附近的程式碼,而不是通過GDB閱讀程式碼,GDB並不是一個好的閱讀工具。以我自己為例,除錯Redis時用GDB除錯,而閱讀程式碼使用的卻是Visual Studio,如下圖所示:
4.7 print 和ptype 命令
通過print命令(簡寫為p)我們可以在除錯過程中方便地檢視變數的值,也可以修改當前記憶體中的變數值。切換當前斷點到堆疊#3,然後列印以下三個變數。
這裡使用print命令分別列印處serve.port、server.ipfd、server.ipfd_count的值,其中server.ipfd顯示“{0\}”,這是GDB顯示字串或字元資料特有的方式,當一個字串變數或者字元陣列或者連續的記憶體值重複若干次,GDB就會以這種模式來顯示以節約空間。
print命令不僅可以顯示變數值,也可以顯示進行一定運算的表示式計算結果值,甚至可以顯示一些函式的執行結果值。
舉個例子,我們可以輸入p &server.port來輸出server.port的地址值,如果在C++物件中,可以通過p this來顯示當前物件的地址,也可以通過p *this來列出當前物件的各個成員變數值,如果有三個變數可以相加(假設變數名分別叫a,b,c),可以使用p a+b+c來列印這三個變數的結果值。
假設func()是一個可以執行的函式,p func()命令可以輸出該變數的執行結果。舉一個最常用的例子,某個時刻,某個系統函式執行失敗了,通過系統變數errno得到一個錯誤碼,則可以使用p strerror(error),將這個對應的文字資訊打印出來,這樣就不用費勁地去man手冊上查詢這個錯誤碼對應的錯誤含義了。
print命令不僅可以輸出表達式結果,同時也可以修改變數的值,我們嘗試將上問中的埠號從6379改成6400試試:
(gdb) p server.port = 6400 $4 = 6400 (gdb) p server.port $5 = 6400
GDB還有另一個命令叫ptype,顧名思義,其含義是“print type”,就是輸出一個變數的型別。例如我們試著輸出Redis堆疊#3的變數server和變數server.port的型別:
type = struct redisServer { pid_t pid; char *configfile; char *executable; char **exec_argv; int dynamic_hz; int config_hz; int hz; redisDb *db; dict *commands; dict *orig_commands; aeEventLoop *el; unsigned int lruclock; int shutdown_asap; int activerehashing; int active_defrag_running; char *requirepass; char *pidfile; int arch_bits; int cronloops; char runid[41]; int sentinel_mode; size_t initial_memory_usage; int always_show_logo; dict *moduleapi; list *loadmodule_queue; int module_blocked_pipe[2]; int port; int tcp_backlog; char *bindaddr[16]; int bindaddr_count; char *unixsocket; mode_t unixsocketperm; int ipfd[16]; ---Type <return> to continue, or q <return> to quit---q Quit (gdb) ptype server.port type = int
可以看到,對於一個複合資料型別的變數,ptype不僅列出了這個變數的型別,而且還詳細地列出了每個成員變數的欄位名,有了這個功能,我們在除錯時就不用可以去程式碼檔案中檢視某個變數的型別定義了。