- C++服務器開發精髓
- 張遠龍
- 9537字
- 2021-07-23 18:22:20
2.5 gdb常用命令詳解——利用gdb調試Redis
為了結合實踐,這里以使用gdb調試Redis源碼為例介紹每個命令,當然,本節只講解gdb中一些常用命令的基礎用法,下節會講解gdb中的一些高級用法。
2.5.1 gdb常用調試命令概覽和說明
下面給出了一個常用命令列表,后面會結合具體的例子詳細介紹每個命令的用法。

上表只列舉了一些常用命令,未列舉一些不常用的命令(如file命令)。不建議讀者刻意記憶這些命令,因為命令較多,建議讀者找幾個程序實際練習一下,這樣就容易記住了。表中“命令縮寫”那一欄,是筆者平時對命令的簡寫輸入,讀者可以采用,也可以不采用。一個命令可以簡寫成什么樣子,gdb沒有強行規定,但讀者在簡寫gdb命令時需要遵循如下兩個規則。
(1)一個命令在簡寫時,不能讓gdb出現多個選擇,若出現多個選擇,gdb就不知道對應哪個命令了。舉個例子,輸入th命令,而th命令對應的命令有thread和thbreak(上表沒有列出),這樣gdb就不知道要使用哪個命令了。需要更具體地輸入,gdb才能識別:


(2)gdb的某些命令雖然對應多個選擇,但是有些命令的簡寫是確定的,比如r是run命令的簡寫,雖然有人輸入r時可能是想使用return命令。
總之,如果記不清楚某個命令的簡寫,就可以直接使用該命令的全寫,每個命令都是很常見的英文單詞,通俗易懂且不難記憶。
2.5.2 用gdb調試Redis前的準備工作
本節逐一介紹上面每個命令的使用方法,會介紹一些很有用的調試細節和使用技巧。如果還不熟悉gdb調試,則建議認真閱讀本節。
為了結合實踐,這里仍以調試 Redis 源碼為例來介紹每個命令。Redis 的最新源碼下載地址可以在 Redis 官網獲得,本書用到的 Redis 版本是 6.0.3。使用 wget 命令將 Redis源碼文件下載下來:

下載完成后解壓縮:

進入生成的redis-6.0.3目錄并使用makefile進行編譯,為了方便調試,我們需要生成調試符號并且關閉編譯器優化選項,操作如下:

-g選項表示生成調試符號,-o0選項表示關閉優化,-j 4選項表示同時開啟4 個進程進行編譯,加快編譯速度。Redis 是純 C 項目,使用的編譯器是 gcc,所以這里設置編譯器的選項時使用的是 CFLAGS 選項;對于 C++項目,使用的編譯器一般是g++,相應的編譯器選項是CXXFLAGS,請注意區別。
如果在編譯過程中出現如下錯誤:


則可以改用以下命令來編譯,這是由于系統沒有安裝jemalloc庫,可以修改編譯參數,讓Redis使用系統默認的malloc而不是jemalloc:

編譯成功后,進入src目錄,使用gdb啟動redis-server程序:

2.5.3 run命令
在默認情況下,gdb+filename只是attach到一個調試文件,并沒有啟動這個程序,我們需要輸入run命令啟動這個程序(run命令被簡寫成r):


這就是 redis-server 的啟動界面。如果程序已經啟動,則再次輸入 run 命令就會重啟程序。我們在 gdb界面按 Ctrl+C組合鍵(界面中的^C)讓程序中斷,再次輸入 r命令,gdb會提示我們是否重啟程序,輸入y確認重啟:

2.5.4 continue命令
在程序觸發斷點或者使用 Ctrl+C 組合鍵中斷后,如果我們想讓程序繼續運行,則只需輸入 continue 命令即可(簡寫成 c)。當然,如果通過 continue 命令讓程序在繼續運行的過程中觸發設置的程序斷點,則程序會在斷點處中斷:

2.5.5 break命令
break 命令即我們添加斷點的命令,可以將其簡寫成 b。我們可以使用以下方式添加斷點:

在以上程序中提到的三種方式是添加斷點的常用方式。舉個例子,對于一般的Linux程序來說,main 函數是程序入口函數,redis-server 也不例外,如果我們知道了函數的名稱,就可以直接利用函數的名稱添加一個斷點。這里以在main函數處設置斷點為例,執行如下命令:


添加好后,使用run命令重啟程序,就可以觸發這個斷點了,gdb會停在斷點處:

redis-server 默認的端口號是 6379。我們知道,無論上層如何封裝,這個端口號最終肯定是通過操作系統的socket API bind函數綁定上去的,我們通過文件搜索,可以找到調用這個函數的文件,它位于anet.c文件第455行,如下圖所示。

使用break命令在這個地方添加一個斷點:


由于程序綁定的端口號是在redis-server啟動時初始化的,所以為了能觸發這個斷點,我們再次使用run命令重啟這個程序,程序第1次會觸發main函數處的斷點,輸入continue命令繼續運行,接著觸發anet.c文件第455行的斷點:


現在斷點停在anet.c文件第455行,我們可以直接使用“break+行號”添加斷點,例如可以在第458、464、466行分別添加一個斷點,看看這個函數執行完畢后走哪個return語句退出。通過“b 行號”形式添加三個斷點,操作如下:

添加三個斷點后,通過continue命令繼續運行程序,程序運行到第466行中斷,說明該函數執行了第466行的return語句:

至此,先調用 bind函數再調用 listen函數,會發現 redis-server已綁定端口并成功開啟了監聽。我們可以再打開一個 Shell 窗口驗證一下,結果證實 6379 端口已經處于監聽狀態:

2.5.6 tbreak命令
break 命令用于添加一個永久斷點;tbreak 命令用于添加一個臨時臨時斷點,其第 1個字母“t”表示“temporarily(臨時的)”,也就是說,通過這個命令添加的斷點是臨時的。臨時斷點,指的是一旦該斷點觸發了一次,就會被自動刪除。添加斷點的方法與上面介紹的break命令一樣,這里不再贅述:

我們在以上代碼中使用tbreak命令在main函數處添加了一個臨時斷點,第1次啟動程序并觸發斷點后,再次重新運行程序,不再觸發斷點,因為這個臨時斷點已被刪除,此時redis-server直接啟動成功。
2.5.7 backtrace與frame命令
backtrace 可簡寫成 bt,用于查看當前所在線程的調用堆棧。現在我們的 redis-server中斷在anet.c文件第466行,可以通過backtrace命令查看當前的調用堆棧。

這里一共有 6層堆棧,堆棧編號為#0~#5,頂層是 main函數,底層是斷點所在的anetListen函數。如果我們想切換到其他堆棧處,則可以使用frame命令,frame命令的使用方法如下:

frame命令可被簡寫成f。這里依次切換至堆棧#1、#2、#3、#4、#5,然后切換回#0,操作如下圖所示。

通過對上面的各個堆棧進行查看,我們可以得出這里的調用層級關系,即main函數在第5128行調用了initServer函數,initServer函數在第2792行調用了listenToPort函數,listenToPort 函數在第 2648 行調用了 anetTcp6Server 函數,anetTcp6Server 函數在第 524行調用了_anetTcpServer函數,_anetTcpServer函數在第501行調用了anetListen函數,當前斷點正好位于anetListen函數中。
2.5.8 info break、enable、disable、delete命令
在程序中加了很多斷點以后,若想查看加了哪些斷點,則可以使用info break命令(簡寫成info b),如下圖所示。

通過上圖,我們可以得到如下信息:目前一共添加了5個斷點,斷點1、2、5已經觸發一次,其他斷點未觸發;每個斷點的位置(所在的文件和行號)、內存地址、斷點啟用和禁用狀態信息也一目了然地展示出來。
如果我們想禁用某個斷點,則使用 disable 斷點編號就可以禁用這個斷點了,被禁用的斷點不會再被觸發;被禁用的斷點可以使用enable斷點編號并重新開啟:


使用disable 1后,第1個斷點Enb一欄的值由y變成n,斷點1不會再觸發,即程序不會在main函數處中斷,程序一直到斷點2處才會停下來:

如果disable和enable命令不加斷點編號,則分別表示禁用和啟用所有斷點:

使用delete編號可以刪除某個斷點,例如delete 2 3表示要刪除斷點2和斷點3:


同樣,如果輸入delete時不加命令號,則表示刪除所有斷點。
2.5.9 list命令
list命令和后面介紹的print命令是gdb調試中使用頻率最高的命令。list命令用于查看當前斷點附近的代碼,可以簡寫成l。我們使用frame命令切換到上文的堆棧#4處,然后輸入list命令查看下效果。

可以發現斷點“停在”第 2792 行,輸入 list 命令以后,會顯示第 2792 行前后的 10行代碼(第2787~2796行)。
再次輸入list命令試一下:


代碼繼續往后顯示 10行(第 2797~2806行),也就是說,第 1次輸入 list命令時會顯示斷點前后的10行代碼,繼續輸入list命令時每次都會接著向后顯示10行代碼,一直到文件結束。
list+命令(即list加號)可以從當前代碼位置向下顯示10行代碼(向文件末尾方向),這和連續輸入多條list命令的效果是一樣的。list-命令(即list 減號)可以從當前代碼位置向上顯示10行代碼(往文件開始方向)。操作效果如下:


list默認顯示的行數量可以通過修改gdb的相關配置來實現,由于我們一般不會修改這個配置值,因此這里就不介紹了。
list不僅可以顯示當前斷點處的代碼,也可以顯示其他文件某一行的代碼,讀者可以在gdb中輸入help list命令來查看更多的用法:

在上面的幫助信息中介紹了可以使用 list FILE:LINENUM 顯示某個文件某一行處的代碼。我們使用 gdb的目的是調試,所以我們更關心的是斷點附近的代碼,而不是通過gdb 閱讀代碼。對于閱讀代碼,gdb 并不是一個好工具。比如,筆者用 gdb 調試 Redis,用VSCode或者Visual Studio閱讀代碼,界面如下圖所示。


2.5.10 print與ptype命令
print命令可以被簡寫成 p。通過 print命令可以在調試過程中方便地查看變量的值,也可以修改當前內存中的變量值。我們切換到堆棧#4,打印以下三個變量:

這里使用print命令分別打印出server.port、server.ipfd、server.ipfd_count的值。其中,server.ipfd顯示{0<repeats 16 times>},這是gdb顯示字符串和字符數組的特有方式,當一個字符串變量、字符數組或者連續的內存值重復若干次時,g d b就會以這種模式顯示,以節約顯示空間。
通過 print命令不僅可以輸出變量的值,也可以輸出特定表達式的計算結果,甚至可以輸出一些函數的執行結果。
舉個例子,我們可以輸入 p&server.port來輸出server.port的地址。對于C++對象,我們可以通過p this顯示當前對象的地址,也可以通過p*this列出當前對象的各個成員變量的值,如果有三個變量可以相加(假設變量名分別為 a、b、c),則可以使用p a+b+c來打印這三個變量的值。
假設func是一個可以執行的函數,則通過p func()命令就可以輸出該變量的執行結果。以一種常見的情形為例,在某個時刻,某個系統函數執行失敗了,通過系統變量 errno得到一個錯誤碼,我們可以使用p strerror(errno)將這個錯誤碼對應的文字信息打印出來,這樣就不用費勁地在man手冊上查找這個錯誤碼對應的錯誤含義了。
通過 print命令不僅可以輸出表達式的結果,還可以修改變量的值,我們嘗試將上文中的端口號從6379改成6400試試:

當然,一個變量的值在修改后能否起作用,要看這個變量的具體位置和作用。舉個例子,對于表達式int a=b/c,如果將c修改成0,程序就會產生除零異常。又如,對于如下代碼:

如果在循環過程中通過print命令將 j的大小由100改成 1000,那么這個循環將輸出 i的值1000次。
print輸出變量值時可以指定輸出格式,命令使用格式如下:

format常見的取值如下。
◎ o octal,八進制顯示。
◎ x hex,十六進制顯示。
◎ d decimal,十進制顯示。
◎ u unsigned decimal,無符號十進制顯示。
◎ t binary,二進制顯示。
◎ f float,浮點值顯示。
◎ a address,內存地址格式顯示(與十六進制相似)。
◎ i instruction,指令格式顯示。
◎ s string,字符串格式顯示。
◎ z hex,zero padded on the left,十六進制左側補0顯示。
對于完整的格式和用法,可以在gdb中輸入help x查看,演示如下:

總結起來,通過 print命令,我們不僅可以查看程序運行過程中各個變量的狀態值,也可以通過臨時修改變量的值來控制程序的行為。
gdb還有另一個命令ptype,顧名思義,其含義是“print type”,就是輸出一個變量的類型。例如試著輸出redis堆棧#4處的變量server和變量server.port的類型:

可以看到,對于一個復合數據類型的變量,ptype不僅列出了這個變量的類型(這里是一個名為redisServer的結構體),而且詳細列出了每個成員變量的字段名,有了這個功能,我們在調試時就不必去代碼文件中翻看某個變量的類型定義了。
2.5.11 info與thread命令
info命令是一個復合指令,可以用來查看當前進程所有線程的運行情況。這里還是以redis-server 進程為例進行演示,使用 delete 命令刪掉所有斷點,然后使用 run 命令重啟redis-server,等程序正常啟動后,我們按 Ctrl+C 組合鍵(代碼中的^C)將程序中斷,然后使用info threads查看進程當前的所有線程信息和這些線程分別中斷在何處。

通過info threads的輸出,我們知道redis-server正常啟動后一共產生了4個線程,其中有 1 個主線程和 3 個工作線程,線程編號(Id 那一列)分別是 1、2、3、4。3 個工作線程(2、3、4)分別阻塞在Linux API pthread_cond_wait處,而主線程1阻塞在epoll_wait處。注意,第 1欄的名稱雖然叫作 Id,但第 1欄的數值并不是線程的 Id;第 3欄有個括號,內容為LWP 5029,這個5029才是當前線程真正的線程Id。那么LWP是什么意思呢?在Linux系統早期的內核里其實不存在真正的線程實現,當時所有的線程都是用進程實現的,這些模擬線程的進程被稱為Light Weight Process(輕量級進程),之后版本的Linux系統內核有了真正的線程實現,但該名稱仍被保留了下來。
讀者可能有個疑問:怎么知道線程1是主線程,線程2、3、4是工作線程呢?是不是因為線程1前面有個星號(*)?錯,線程編號前面的星號表示gdb當前作用于哪個線程,不是說標了星號就是主線程。當前有 4 個線程,也就有 4 個調用堆棧,如果此時輸入backtrace 命令查看調用堆棧,則由于 gdb當前作用于線程 1,所以通過 backtrace命令顯示的是線程1的調用堆棧:

看到了吧!堆棧#4的main函數也證明了線程編號為1的線程是主線程。
那么如何切換到其他線程呢?我們可以通過thread線程編號命令切換到指定的線程。例如我們想切換到線程2,則只需輸入thread 2命令即可,接著輸入bt命令就能查看這個線程的調用堆棧了:


所以利用info threads命令可以調試多線程程序。當然,使用gdb調試多線程程序存在一個很麻煩的問題,在之后的章節中會有介紹。
將gdb切換到哪個線程,哪個線程前面就被加上星號標記,例如我們把gdb當前作用的線程切換到線程2上之后,線程2前面就被加上了星號。

info命令還可以用來查看當前函數的參數值,組合命令是info args。我們找個函數來試一下這個命令:

以上代碼片段先切回到主線程 1,然后切回到堆棧#2。堆棧#2 調用處的函數是aeProcessEvents,這個函數一共有兩個參數,分別是 eventLoop和 tvp,使用 info args命令可以輸出這兩個參數的值。eventLoop 是一個指針類型的參數,對于指針類型的參數,gdb默認會輸出該變量的指針值。如果想輸出該指針指向的對象的值,則可以在變量名前面加上星號,即解引用操作符(*),這里使用p*eventLoop即可:

如果還要查看其成員值,則繼續使用變量名->字段名即可(如p eventLoop->maxfd),我們在前面介紹print命令時已經介紹過這種用法,這里不再贅述。
info命令的功能遠非上面介紹的三種,讀者可以在 gdb 中輸入 help info 查看更多的info組合命令的用法。
2.5.12 next、step、until、finish、return、jump命令
之所以把這幾個命令放在一起,是因為它們是用gdb調試程序時最常用的幾個控制流命令。next命令可被簡寫為n,用于讓gdb跳到下一行代碼。這里跳到下一行代碼不是說一定要跳到離代碼最近的下一行,而是根據程序邏輯跳轉到相應的位置。舉個例子:

如果 gdb中斷在以上代碼第 2行,此時輸入 next命令,則gdb將跳到第7行,因為這里的if條件不滿足。
在gdb命令行界面直接按下回車鍵,默認是將最近一條命令重新執行一遍。所以,當我們使用 next 命令單步調試時,不必反復輸入 n 命令,在輸入一次 n命令之后想再次輸入next命令時直接按回車鍵就可以了。


上面的執行過程等價于輸入第1個n后直接回車:

next命令的調試術語叫“單步步過(step over)”,即遇到函數調用時不進入函數體內部,而是直接跳過。下面說的step命令就是“單步步入(step into)”,顧名思義,就是遇到函數調用時進入函數內部。step可被簡寫為 s。舉個例子,在 redis-server的 main函數中有個spt_init(argc,argv)函數調用,我們停在這一行時,輸入s將進入這個函數內部:

這里演示一下,先用b main在main函數處加一個斷點,然后使用r命令重新跑程序,會觸發剛才加在main函數處的斷點,然后使用n命令讓程序走到spt_init(argc,argv)函數調用處,再輸入s命令就可以進入該函數中了:


說到step命令,我們還有一個需要注意的地方,就是當函數的參數也是函數調用時,使用step命令會依次進入各個函數中,順序是怎樣的呢?舉個例子,看看下面這段代碼:

以上代碼的程序入口函數是 main函數。在第 22行中,func3使用 func1和 func2 的返回值作為自己的參數。我們在第22行輸入step命令,會先進入哪個函數中呢?這就需要補充一個知識點了——函數調用方式。
常用的函數調用方式有__cdecl 和__stdcall,C++的非靜態成員函數的調用方式是__thiscall,函數參數的傳遞在本質上是函數參數的入棧。對于這三種函數調用方式,參數的入棧順序都是從右向左的。在這段代碼中,由于沒有顯式標明函數的調用方式,所以采用的函數調用方式是__cdecl,這是C/C++中全局函數和類靜態方法的默認調用方式。
因此,當我們在第22行代碼處輸入step時,先進入的是func2;當從func2返回時再次輸入step命令,會接著進入func1;當從func1返回時,兩個參數已經計算出來了,這時會最終進入func3。讀者只有理解這一點,在遇到這樣的代碼時,才能根據需要進入自己想要的函數中調試。
在實際調試時,我們在某個函數中調試一會兒后,不希望再一步步地執行到函數返回處,而是希望直接執行完當前函數并回到上一層調用處,這時可以使用finish命令。與finish命令類似的還有return命令。return命令用于結束執行當前函數,同時指定該函數的返回值。這里需要注意二者的區別:finish命令用于執行完整的函數體,然后正常返回到上層調用中;return命令用于立即從函數的當前位置結束并返回到上層調用中,也就是說,如果使用了return命令,則在當前函數還有剩余的代碼未執行完畢時,也不會再執行了。我們用一個例子來驗證一下:

在main函數處加一個斷點,然后運行程序;在第15行使用step命令進入func函數中;接著使用next命令單步運行到代碼第8行,直接輸入return命令,這樣func函數剩余的代碼就不會接著執行了,所以printf("b=%d.\n",b);行沒有輸出。同時,由于我們沒有在return命令中指定這個函數的返回值,所以最終在 main函數中得到的變量 c的值是一個臟數據。這就驗證了上面的結論:return命令立即從函數當前位置結束并返回到上一層調用中。驗證過程如下:



我們再次用return命令指定一個值試一下,這樣我們得到的變量c的值應該就是我們指定的值了。驗證過程如下:

仔細觀察以上代碼,我們用 return命令修改了函數的返回值,當使用 print命令打印c的值時,c的值也確實被修改成了9999。
再次對比使用finish命令結束函數執行的結果:

結果和我們預期的一樣,finish正常結束了我們的函數,剩余的代碼也會被正常執行。因此c的值是17。
實際調試時,還有一個 until 命令,可被簡寫為u,我們使用這個命令讓程序運行到指定的行停下來。還是以redis-server的代碼為例:


以上是redis-server中initServer函數的部分代碼,位于文件server.c中。當gdb停在第2740行時(注意:這里的行號以在gdb調試器中顯示的為準,不是源碼文件中的行號,由于存在條件編譯,部分代碼可能不會被編譯到可執行文件中,所以實際的調試符號文件中的行號與源碼文件中的行號可能不會完全一致),我們可以通過輸入u 2774命令讓gdb直接跳到第 2774行,這樣就能快速執行完第 2740~2774 行中間的代碼(不包括第 2774行)。當然,我們可以先在第2774行加一個斷點,然后使用continue命令運行到這一行來達到同樣的效果,但是使用until命令顯然更方便:


jump命令的基本用法如下:

location可以是程序的行號或者函數的地址,jump會讓程序執行流跳轉到指定的位置執行,其行為也是不可控的,例如跳過了某個對象的初始化代碼,直接執行操作該對象的代碼,可能會導致程序崩潰或其他意外操作。jump命令可被簡寫為j,但是不可被簡寫為jmp,使用該命令時有一個注意事項:如果 jump 跳轉到的位置沒有設置斷點,那么 gdb執行完跳轉操作后會繼續向下執行。舉個例子:

假設我們的斷點的初始位置在第3行(代碼A),那么這時我們使用jump 6,程序會跳過代碼B和C的執行,執行完代碼D(跳轉點),程序并不會停在第6行,而是繼續執行后續代碼,因此如果我們想查看執行跳轉處代碼的結果,就需要在第6、7或8行代碼處設置斷點。
通過 jump 命令除了可以跳過一些代碼的執行,還可以執行一些我們想執行的代碼,而這些代碼在正常邏輯下可能并不會被執行。當然,根據實際的程序邏輯,可能會產生一些非預期結果,這需要我們自行斟酌使用。舉個例子,假設現在有如下代碼:

我們在第4、14行設置一個斷點,在觸發第4行的斷點后,在正常情況下程序執行流會走 else 分支,我們可以使用 jump 7 強行讓程序執行 if分支,接著 gdb 會因觸發第 14行的斷點停下來,此時我們接著執行jump 11,程序會將else分支中的代碼重新執行一遍。整個操作過程如下:

redis-server在入口函數main處調用了initServer,我們使用b initServer、b 2753、b 2755分別在這個函數入口處、第2753行、第2755行增加3個斷點,然后使用run命令重新運行程序。觸發第1個斷點后,輸入c命令繼續運行,然后觸發第2753行的斷點,接著輸入jump 2755。以下是操作過程:


程序跳過了第2754行的代碼。第2754行的代碼用于獲取當前的進程id:

由于這一行被跳過,所以 server.pid 的值應該是一個無效的值,我們可以使用 print命令將這個值打印出來看一下:

結果是0,這個0值是Redis初始化時設置的。
gdb的jump命令的作用,與使用Visual Studio調試時通過鼠標將程序當前的執行點從一個位置拖到另一個位置的作用一樣。

2.5.13 disassemble命令
在某些場景下,我們可能要通過查看某段代碼的匯編指令去排查問題,或者在調試一些不含調試信息的 release 版程序時,只能通過反匯編代碼定位問題。在此類場景下,disassemble 命令就派上用場了。disassemble 會輸出當前函數的匯編指令,例如在 Redis的initServer函數中執行該命令,會輸出initServer函數的匯編指令,操作如下:

gdb的反匯編格式默認為AT&T格式,可以通過show disassembly-flavor查看當前的反匯編格式。如果習慣閱讀intel匯編格式,則可以使用set disassembly-flavor intel命令來設置。操作如下:

disassemble 命令在程序崩潰后產生 core 文件,且在無對應的調試符號時非常有用,此時可以通過分析匯編代碼排查一些問題。
2.5.14 set args與show args命令
很多程序都需要我們傳遞命令行參數。在gdb調試中使用gdb filename args這種形式給被調試的程序傳遞命令行參數是行不通的。正確的做法是在用gdb attach程序后,使用run 命令之前,使用 set args 命令行參數來指定被調試程序的命令行參數。還是以redis-server 為例,redis 在啟動時可以指定一個命令行參數,即它的配置文件,位于redis-server 文件的上一層目錄下。所以我們可以在 gdb 中這樣傳遞這個參數:set args../redis.conf,可以通過show args查看命令行參數是否設置成功:

如果在單個命令行參數之間有空格,則可以使用引號將參數包裹起來:

如果想清除已經設置好的命令行參數,則使用set args不加任何參數即可:

2.5.15 watch命令
watch是一個強大的命令,可以用來監視一個變量或者一段內存,當這個變量或者該內存處的值發生變化時,gdb就會中斷。監視某個變量或者某個內存地址會產生一個觀察點(watch point)。
比如有一個面試題:有一個變量的值被意外修改,單步調試或者挨個檢查使用該變量的代碼,工作量非常大,那么如何快速定位該變量被修改的位置呢?其實,面試官想要的答案是“通過數據斷點”。watch命令可能通過添加硬件斷點來達到監視數據變化的目的。watch命令的使用方式是watch變量名或內存地址,一個觀察點一般有以下幾種格式。
(1)整形變量:

(2)指針類型:


注意:watch p與**watch*p**是有區別的,前者是查看*(&p),是p變量本身;后者是p所指的內存的內容,一般是我們所需的,我們在大多數情況下要看某內存地址上的數據如何變化。
(3)監視一個數組或內存區間:

這里是對buf的128個數據進行了監視。
需要注意的是:當設置的觀察點是一個局部變量時,局部變量失效后,觀察點也會失效。例如在觀察點失效時,gdb可能會提示如下信息:

2.5.16 display命令
display 命令用于監視變量或者內存的值,每次 gdb 中斷,都會自動輸出這些被監視變量或內存的值。例如,某個程序有一些全局變量,在每次觸發斷點后gdb中斷下來時,我們都希望自動輸出這些全局變量的最新值,這時就可以使用display命令了。display命令的使用格式是display 變量名/內存地址/寄存器名:

在以上代碼中,我們使用 display 命令分別監視寄存器 ebp 和寄存器 eax,要求 ebp寄存器分別使用十進制和十六進制兩種形式輸出其值,這樣每次 gdb中斷下來時,都會自動將這些寄存器的值輸出。我們可以使用info display查看當前已經監視了哪些值,使用delete display清除全部被監視的變量,使用delete display 編號移除對指定變量的監視。操作演示如下:

2.5.17 dir命令
讀者可能會遇到這樣的場景:使用gdb調試時,生成可執行文件的機器和實際執行該可執行程序的機器不是同一臺機器,例如大多數企業產生目標服務程序的機器是編譯機器,即發版機,然后把發版機產生的可執行程序拿到生產機器上執行。這時如果可執行程序崩潰,我們用gdb調試core文件時,gdb就會提示“No such file or directory”,如下所示:

或者由于一些原因,編譯時的源碼文件被挪動了位置,使用gdb調試時也會出現上述情況。
gcc/g++編譯出來的可執行程序并不包含完整的源碼,-g 只是加了一個可執行程序與源碼之間的位置映射關系,我們可以通過dir命令重新定位這種關系。
dir命令的使用格式如下:

SourcePath1、SourcePath2、SourcePath3 指的就是需要設置的源碼目錄,gdb 會依次到這些目錄下搜索相應的源文件。
以上面的錯誤提示為例,原來的 AsyncLog.cpp 文件位于/home/flamingoserver/base/目錄下,由于這個目錄被挪動,所以 gdb提示找不到該文件。現在假設該文件被移動到/home/zhangyl/flamingoserver/base/目錄下,我們只需在 gdb調試中執行 dir/home/zhangyl/flamingoserver/base/,即可重定向可執行程序與源碼的位置關系:

(gdb)
使用 dir命令重新定位源文件的位置之后,gdb就不會再提示這樣的錯誤了,我們此時也可以使用gdb的其他命令(如list命令)查看源碼。
如果要查看當前設置了哪些源碼搜索路徑,則可以使用show dir命令:

dir命令不加參數時,表示清空當前已設置的源碼搜索路徑:

- HTML5+CSS3+JavaScript從入門到精通:上冊(微課精編版·第2版)
- 多媒體CAI課件設計與制作導論(第二版)
- 碼上行動:零基礎學會Python編程(ChatGPT版)
- 技術領導力:程序員如何才能帶團隊
- Scala Reactive Programming
- Apache Camel Developer's Cookbook
- Raspberry Pi Robotic Projects(Third Edition)
- Unity Android Game Development by Example Beginner's Guide
- SSH框架企業級應用實戰
- WordPress Search Engine Optimization(Second Edition)
- Jakarta EE Cookbook
- Python程序設計
- Continuous Integration,Delivery,and Deployment
- Performance Testing with JMeter 3(Third Edition)
- Hands-On Game Development Patterns with Unity 2019