1. GDB介紹
GDB是GNU Debugger的簡稱,其作用是可以在程序運行時,檢測程序正在做些什么。GDB程序自身是使用C和C++程序編寫的,但可以支持除C和C++之外很多編程語言的調(diào)試。GDB原生支持調(diào)試的語言包含:
?C
?C++
?D
?Go
?Object-C
?OpenCL C
?Fortran
?Pascal
?Rust
?Modula-2
?Ada
此外,通過擴展GDB,也可以用來調(diào)試Python語言。
使用GDB,我們可以方便地進行如下任務:
?如果程序崩潰后產(chǎn)生了core dump文件,gdb可以通過分析core dump文件,找出程序crash的位置,調(diào)用堆棧等用于找出問題原因的關鍵信息
?在程序運行時,GDB可以檢測當前程序正在做什么事情
?在程序運行時,修改變量的值
?可以使程序在特定條件下中斷
?監(jiān)視內(nèi)存地址變動
?分析程序Crash后的core文件
GDB是了解三方中間件,無源碼程序,解決程序疑難雜癥的利器。使用GDB,可以了解程序在運行時的方方面面。尤其對于在測試(Test),集成(SIT),驗收(UAT),預發(fā)布(Staging)等環(huán)境下的問題調(diào)查和解決,GDB有著日志無法比擬的優(yōu)勢。此外,GDB還非常適合對多種開發(fā)語言混合的程序進行調(diào)試。
GDB不適合用來做什么:
?GDB可以用來輔助調(diào)試內(nèi)存泄露問題,但GDB不能用于內(nèi)存泄露檢測
?GDB可以用來輔助程序性能調(diào)優(yōu),但GDB不能用于程序性能問題分析
?GDB不是編譯器,不能運行有編譯問題的程序,也不能用來調(diào)試編譯問題
2. 安裝GDB
2.1. 從已發(fā)布的二進制包安裝
在基于Debian的Linux系統(tǒng),可以使用apt-get命令方便地安裝GDB
apt-get update
apt-get install gdb
復制代碼
2.2. 從源代碼安裝
前置條件
# 安裝必要的編譯工具
apt-get install build-essential
復制代碼
首先,我們需要下載GDB的源碼。官網(wǎng)下載源碼的地址是:
ftp.gnu.org/gnu/gdb/
# 下載源代碼
wget http://ftp.gnu.org/gnu/gdb/gdb-9.2.tar.gz
# 解壓安裝包
tar -xvzf gdb-9.2.tar.gz
# 編譯GDB
cd gdb-7.11
mkdir build
cd build
../configure
make
# 安裝GDB
make install
# 檢查安裝結(jié)果
gdb --version //輸出
復制代碼
3. 準備使用GDB
3.1. 在docker容器內(nèi)使用GDB
GDB需要使用ptrace 方法發(fā)送PTRACE_ATTACH請求給被調(diào)試進程,用來監(jiān)視和控制另一個進程。
Linux 系統(tǒng)使用
/proc/sys/kernel/yama/ptrace_scope設置來對ptrace施加安全控制。默認ptrace_scope的設置的值是1。默認設置下,進程只能通過PTRACE_ATTACH請求,附加到子進程。當設置為0時,進程可以通過PTRACE_ATTACH請求附加到任何其它進程。
在docker容器內(nèi),即使是root用戶,仍有可能沒有修改這個文件的權限。使得在使用GDB調(diào)試程序時會產(chǎn)生“ptrace: Operation not permitted “錯誤。
為了解決docker容器內(nèi)使用GDB的問題,我們需要使用特權模式運行docker容器,以便獲得修改
/proc/sys/kernel/yama/ptrace_scope文件的權限。
# 以特權模式運行docker容器
docker run --privileged xxx
# 進入容器,輸入如下指令改變PTRACE_ATTACH請求的限制
echo 0 > /proc/sys/kernel/yama/ptrace_scope
復制代碼
3.2. 啟用生成core文件
默認情況下,程序Crash是不生成core文件的,因為默認允許的core文件大小為0。
為了在程序Crash時,能夠生成core文件來幫助排查Crash的原因,我們需要修改允許的core文件大小設置
# 查看當前core文件大小設置
ulimit -a
# 設置core文件大小為不限制
ulimit -c unlimited
# 關閉core文件生成功能
ulimit -c 0
復制代碼
修改core文件設置后,再次查看core文件的設置時,會看到下面的結(jié)果
這樣,當程序Crash時,會在程序所在的目錄,生成名稱為core.xxx的core文件。
當程序運行在Docker容器內(nèi)時,在容器內(nèi)進行上述設置后,程序Crash時仍然無法生成core文件。這時需要我們在Docker容器的宿主機上,明確指定core文件的生成位置。
# 當程序Crash時,在/tmp目錄下生成core文件
echo '/tmp/core.%t.%e.%p' > /proc/sys/kernel/core_pattern
復制代碼
設置中的字段的含義如下:
?/tmp 存放core文件的目錄
?core 文件名前綴
?%t 系統(tǒng)時間戳
?%e 進程名稱
?%p 進程ID
3.3. 生成調(diào)試符號表
調(diào)試符號表是二進制程序和源代碼的變量,函數(shù),代碼行,源文件的一個映射。一套符號表對應特定的一套二進制程序,如果程序發(fā)生了變化,那么就需要一套新的符號表。
如果沒有調(diào)試符號表,包含代碼位置,變量信息等很多調(diào)試相關的能力和信息將無法使用。在編譯時加入-ggdb編譯選項,就會在生成的二進制程序中加入符號表,此時生成的二進制程序的大小會有顯著的增加。
-ggdb 用來生成針對gdb的調(diào)試信息,也可以使用-g來代替
另外,只要條件允許,建議使用-O0來關閉編譯優(yōu)化,用來避免調(diào)試時,源代碼和符號表對應不上的奇怪問題。
-O0 關閉編譯優(yōu)化
3.4. 使用screen來恢復會話
GDB調(diào)試依賴于GDB控制臺來和進程進行交互,如果我們的連接終端關閉,那么原來的控制臺就沒有辦法再使用了。此時我們可以通過開啟另一個終端,關閉之前的GDB進程,并重新attach到被調(diào)試進程,但此時的斷點,監(jiān)視和捕獲都要重新設置。另一種方法就是使用screen。使用screen運行的程序,可以完全恢復之前的會話,包括GDB控制臺。
# 安裝screen
apt install screen
# 查看安裝結(jié)果
screen -v //output: Screen version 4.08.00 (GNU) 05-Feb-20
# 使用screen啟動調(diào)試
screen gdb xxx
# 查看screen會話列表
screen -ls
# 恢復screen會話
screen -D -r [screen session id]
復制代碼
4. 啟動GDB的幾種方式
4.1. 使用GDB加載程序,在GDB命令行啟動運行
這是經(jīng)典的使用GDB的方式。程序可以通過GDB命令的參數(shù)來加載,也可以在進入GDB控制臺后,通過file命令來加載。
# 使用GDB加載可執(zhí)行程序
gdb [program]
# 使用GDB加載可執(zhí)行程序并傳遞命令行參數(shù)
gdb --args [program] [arguments]
# 開始調(diào)試程序
(gdb) run
# 傳遞命令行參數(shù)并開始調(diào)試程序
(gdb) run arg1 arg2
# 開始調(diào)試程序并在main函數(shù)入口中斷
(gdb) start
# 傳遞命令行參數(shù),開始調(diào)試程序并在main函數(shù)入口中斷
(gdb) start arg1 arg2
復制代碼
4.2. 附加GDB到運行中的進程
GDB可以直接通過參數(shù)的方式,附加到一個運行中的進程。也可以在進入GDB控制臺后,通過attach命令附加到進程。
需要注意的是一個進程只允許附加一個調(diào)試進程,如果被調(diào)試的進程當前已經(jīng)出于被調(diào)試狀態(tài),那么要么通過detach命令來解除另一個GDB進程的附加狀態(tài),要么強行結(jié)束當前附加到進程的GDB進程,否則不能通過GDB附加另一個調(diào)試進程。
# 通過GDB命令附加到進程
gdb --pid [pid]
# 在GDB控制臺內(nèi),通過attach命令附加的進程
gdb
(gdb) attach [pid]
復制代碼
4.3. 調(diào)試core文件
在程序Crash后,如果生成了core文件,我們可以通過GDB加載core文件,調(diào)試發(fā)生異常時的程序信息。core文件是沒有限制當前機器相關信息的,我們可以拷貝core文件到另一臺機器進行core分析,但前提是產(chǎn)生core文件的程序的符號表,需要和分析core文件時加載的程序的符號表保持一致。
使用GDB調(diào)試core文件
# 使用GDB加載core文件進行異常調(diào)試
gdb --core [core file] [program]
復制代碼
4.4. 使用GDB加載程序并自動運行
在自動化測試場景中,需要程序能夠以非中斷的方式流暢地運行,同時又希望附加GDB,以便隨時可以了解程序的狀態(tài)。這時我們可以使用--ex參數(shù),指定GDB完成程序加載后,自動運行的命令。
# 使用GDB加載程序,并在加載完成后自動運行run命令
gdb --ex r --args [program] [arguments]
復制代碼
5. 使用GDB
5.1. 你好,GDB
我們先從一個Hello world的例子,通過GDB設置斷點來調(diào)試程序,近距離接觸下GDB。
首先使用記事本或其它工具編寫下面的main.cc代碼:
#include <iostream>
#include <string>
int main(int argc, char *argv[]) {
std::string text = “Hello world”;
std::cout << text << std::endl;
return 0;
}
復制代碼
接下來我們使用g++編譯器編譯源碼,并設置-ggdb -O0編譯選項。
g++ -ggdb -O0 -std=c++17 main.cc -o main
復制代碼
生成可執(zhí)行程序后,我們使用GDB加載可執(zhí)行程序,并設置斷點。
# 使用gdb加載main
gdb main
# 在main.cc源文件的第六行設置斷點
(gdb) b main.cc:6
# 運行程序
(gdb) run
復制代碼
之后,程序會運行到斷點位置并停下來,接下來我們使用一些常用的GDB指令來檢查程序的當前狀態(tài)
# 輸出text變量數(shù)據(jù) “Hello world“
(gdb) p text
# 輸出局部變量列表,當前斷點位置只有一個text局部變量
(gdb) info locals
# 輸出當前棧幀的方法參數(shù),當前棧幀函數(shù)是main,參數(shù)包含了argc和argv
(gdb) info args
# 查看堆棧信息,當前只有一個棧幀
(gdb) bt
# 查看當前棧幀附近的源碼
(gdb) list
# 繼續(xù)運行程序
(gdb) c
# 退出GDB
(gdb) q
復制代碼
5.2. Segmentation Fault問題排查
Segmentation Fault是進程訪問了由操作系統(tǒng)內(nèi)存保護機制規(guī)定的受限的內(nèi)存區(qū)域觸發(fā)的。當發(fā)生Segmentation Fault異常時,操作系統(tǒng)通過發(fā)起一個“SIGSEGV”信號來終止進程。此外,Segmentation Fault不能被異常捕捉代碼捕獲,是導致程序Crash的常見誘因。
對于C&C++等貼近操作系統(tǒng)的開發(fā)語言,由于提供了靈活的內(nèi)存訪問機制,所以自然成為了Segmentation Fault異常的重災區(qū),由于默認的Segmentation Fault異常幾乎沒有詳細的錯誤信息,使得開發(fā)人員處理此類異常時變得更為棘手。
在實際開發(fā)中,使用了未初始化的指針,空指針,已經(jīng)被回收了內(nèi)存的指針,棧溢出,堆溢出等方式,都會引發(fā)Segmentation Fault。
如果啟用了core文件生成,那么當程序Crash時,會在指定位置生成一個core文件。通過使用GDB對core文件的分析,可以幫助我們定位引發(fā)Segmentation Fault的原因。
為了模擬Segmentation Fau我們首先在main.cc中添加一個自定義類Employee
class Employee{
public:
std::string name;
};
復制代碼
然后編寫代碼,模擬使用已回收的指針,從而引發(fā)的Segmentation Fault異常
void simulateSegmentationFault(const std::string& name) {
try {
Employee *employee = new Employee();
employee->name = name;
std::cout << "Employee name = " << employee->name << std::endl;
delete employee;
std::cout << "After deletion, employee name = " << employee->name << std::endl;
} catch (...) {
std::cout << "Error occurred!" << std::endl;
}
}
復制代碼
最后,在main方法中,添加對simulateSegmentationFault方法的調(diào)用
在main方法中,添加對simulateSegmentationFault方法的調(diào)用
int main(int argc, char *argv[]) {
std::string text = "Hello world";
std::cout << text << std::endl;
simulateSegmentationFault(text);
return 0;
}
復制代碼
編譯并執(zhí)行程序,我們會得到如下的運行結(jié)果
$ ./main
Hello world
Employee name = Hello world
Segmentation fault (core dumped)
復制代碼
從結(jié)果上來看,首先我們的異常捕獲代碼對于Segmentation Fault無能為力。其次,發(fā)生異常時沒有打印任何對我們有幫助的提示信息。
由于代碼非常簡單,從日志上很容易了解到問題發(fā)生在”std::cout << "After deletion, employee name = " << employee->name << std::endl;” 這一行。在實際應用中,代碼和調(diào)用都非常復雜,很多時候僅通過日志沒有辦法準確定位異常發(fā)生的位置。這時,就輪到GDB出場了
# 使用GDB加載core文件
gdb --core [core文件路徑] main
//對于沒有生成core文件的情況,請參考3.2. 啟用生成core文件
復制代碼
注意其中的”Reading symbols from main..”,如果接下來打印了找不到符號表的信息,說明main程序中沒有嵌入調(diào)試符號表,此時變量,行號,等信息均無法獲取。若要生成調(diào)試符號表,可以參考 “3.3. 生成調(diào)試符號表”。
成功加載core文件后,我們首先使用bt命令來查看Crash位置的錯誤堆棧。從堆棧信息中,可以看到__GI__IO_fwrite方法的buf參數(shù)的值是0x0,這顯然不是一個合法的數(shù)值。序號為5的棧幀,是發(fā)生異常前,我們自己的代碼壓入的最后一個棧幀,信息中甚至給出了發(fā)生問題時的調(diào)用位置在main.cc文件的第15行(main.cc:15),我們使用up 5 命令向前移動5個棧幀,使得當前處理的棧幀移動到編碼為5的棧幀。
# 顯示異常堆棧
(gdb) bt
#向上移動5個棧幀
(gdb) up 5
復制代碼
此時可以看到傳入的參數(shù)name是沒有問題的,使用list命令查看下問題調(diào)用部分的上下文,再使用info locals命令查看調(diào)用時的局部變量的情況。最后使用 p *employe命令,查看employee指針指向的數(shù)據(jù)
# 顯示所有的參數(shù)
(gdb) info args
# 顯示棧幀所在位置的上下文代碼
(gdb) list
# 顯示所有的局部變量
(gdb) info locals
# 打印employee指針的數(shù)據(jù)
(gdb) p *employee
復制代碼
此時可以看到在main.cc代碼的第15行,使用std::cout輸出Employee的name屬性時,employee指針指向的地址的name屬性已經(jīng)不再是一個有效的內(nèi)存地址(0x0)。
5.3. 程序阻塞問題排查
程序阻塞在程序運行中是非常常見的現(xiàn)象。并不是所有的阻塞都是程序產(chǎn)生了問題,阻塞是否是一個要解決的問題,在于我們對于程序阻塞的預期。比如一個服務端程序,當完成了必要的初始化后,需要阻塞主線程的繼續(xù)執(zhí)行,避免服務端程序執(zhí)行完main方法后退出。就是正常的符合預期的阻塞。但是如果是一個客戶端程序,執(zhí)行完了所有的任務后在需要退出的時候,還處于阻塞狀態(tài)無法關閉進程,就是我們要處理的程序阻塞問題。除了上面提到的程序退出阻塞,程序阻塞問題一般還包括:
?并發(fā)程序中產(chǎn)生了死鎖,線程無法獲取到鎖對象
?遠程調(diào)用長時間阻塞無法返回
?程序長時間等待某個事件通知
?程序產(chǎn)生了死循環(huán)
?訪問了受限的資源和IO,出于排隊阻塞狀態(tài)
對于大多數(shù)阻塞來說,被阻塞的線程會處于休眠狀態(tài),放置于等待隊列,并不會占用系統(tǒng)的CPU時間。但如果這種行為不符合程序的預期,那么我們就需要查明程序當前在等待哪個鎖對象,程序阻塞在哪個方法,程序在訪問哪個資源時卡住了等問題.
下面我們通過一個等待鎖釋放的阻塞,使用GDB來分析程序阻塞的原因。首先引入線程和互斥鎖頭文件
#include <thread>
#include <mutex>
復制代碼
接下來我們使用兩個線程,一個線程負責加鎖,另一個線程負責解鎖
std::mutex my_mu;
void thread1_func() {
for (int i = 0; i < 5; ++i) {
my_mu.lock();
std::cout << "thread1 lock mutex succeed!" << std::endl;
std::this_thread::yield();
}
}
void thread2_func() {
for (int i = 0; i < 5; ++i) {
my_mu.unlock();
std::cout << "thread2 unlock mutex succeed!" << std::endl;
std::this_thread::yield();
}
}
void simulateBlocking() {
std::thread thread1(thread1_func);
std::thread thread2(thread2_func);
thread1.join();
thread2.join();
}
復制代碼
最后,重新編譯main程序,并在g++編譯時,加入lpthread鏈接參數(shù),用來鏈接pthread庫
g++ -ggdb -O0 -std=c++17 main.cc -o main -lpthread
復制代碼
直接運行main程序,此時程序大概率會阻塞,并打印出類似于如下的信息
為了調(diào)查程序阻塞的原因,我們使用命令把gdb關聯(lián)到運行中的進程
gdb --pid xxx
復制代碼
進入GDB控制臺后,依舊是先使用bt打印當前的堆棧信息
# 打印堆棧信息
(gdb) bt
# 直接跳轉(zhuǎn)到我們的代碼所處的編號為2的棧幀
(gdb) f 2
# 查看代碼
(gdb) list
復制代碼
此時我們通過查看堆棧信息,知道阻塞的位置是在main.cc的45行,即thread1.join()沒有完成。但這并不是引發(fā)阻塞的直接原因。我們還需要繼續(xù)調(diào)查為什么thread1沒有結(jié)束
# 查看所有運行的線程
(gdb) info threads
# 查看編號為2的線程的堆棧
(gdb) thread apply 2 bt
# 切換到線程2
(gdb) thread 2
復制代碼
由于示例程序比較簡單,所有運行的線程只有兩個,我們可以很容易地找到我們需要詳細調(diào)查的thread1所在的線程。
當進程當前運行較多線程時,想找到我們程序中的特定線程并不容易。info threads中給出的線程ID,是GDB的thread id,和thread1線程的id并不相同。而LWP中的線程ID,則是系統(tǒng)賦予線程的唯一ID,同樣和我們在進程內(nèi)部直接獲取的線程ID不相同。這里我們通過thread apply命令,直接調(diào)查編號為2的線程的堆棧信息,確認了其入口函數(shù)是thread1_func,正是我們要找到thread1線程。我們也可以通過thread apply all bt命令,查看所有線程的堆棧信息,用來查找我們需要的線程。更簡單的方式是調(diào)用gettid函數(shù),獲取操作系統(tǒng)為線程分配的輕量進程ID(LWP)。
接下來,我們調(diào)查thread1的堆棧,找到阻塞的位置并調(diào)查阻塞的互斥鎖my_mu的信息,找到當前持有該鎖的線程id(Linux系統(tǒng)線程ID),再次通過info threads查到持有鎖的線程。最后發(fā)現(xiàn)是因為當前線程持有了互斥鎖,當再次請求獲取鎖對象my_mu時,由于my_mu不可重入,導致當前線程阻塞,形成死鎖。
# 查看thread1的堆棧
(gdb) bt
# 直接跳轉(zhuǎn)到我們的代碼所處的棧幀
(gdb) f 4
# 查看鎖對象my_mu
(gdb) p my_mu
# 確認持有鎖的線程
(gdb) info threads
復制代碼
5.4. 數(shù)據(jù)篡改問題排查
數(shù)據(jù)篡改不一定會引發(fā)異常,但很可能會導致業(yè)務結(jié)果不符合預期。對于大量使用了三方庫的項目來說,想知道數(shù)據(jù)在哪里被修改成了什么,并不是一件容易的事。對于C&C++來說,還存在著指針被修改后,導致指針原來指向的對象可能無法回收的問題。單純使用日志,想要發(fā)現(xiàn)一個變量在何時被哪個程序修改成了什么,幾乎是不可能的事,通過使用GDB的監(jiān)控斷點,我們可以方便地調(diào)查這類問題。
我們?nèi)匀皇褂枚嗑€程模式,一個線程模擬讀取數(shù)據(jù),當發(fā)現(xiàn)數(shù)據(jù)被修改后,打印一條出錯信息。另一個線程用來模擬修改數(shù)據(jù)。
這里我們使用的Employee對象的原始的name和修改后的name都大于15個字符,如果長度小于這個數(shù)值,你將會觀察到不一樣的結(jié)果。
void check_func(Employee& employee) {
auto tid = gettid();
std::cout << "thread1 " << tid << " started" << std::endl;
while (true) {
if (employee.name.compare("origin employee name") != 0) {
std::cout << "Error occurred, Employee name changed, new value is:" << employee.name << std::endl;
break;
}
std::this_thread::yield();
}
}
void modify_func(Employee& employee) {
std::this_thread::sleep_for(std::chrono::milliseconds(0));
employee.name = std::string("employee name changed");
}
void simulateDataChanged() {
Employee employee("origin employee name");
std::thread thread1(check_func, std::ref(employee));
std::thread thread2(modify_func, std::ref(employee));
thread1.join();
thread2.join();
}
復制代碼
在main方法中,加入simulateDataChanged方法的調(diào)用,之后編譯并運行程序,會得到如下的結(jié)果:
現(xiàn)在,我們假設修改了name屬性的modify_func在一個三方庫中,我們對其內(nèi)部實現(xiàn)不了解。我們需要要通過GDB,找到誰動了employee對象的name屬性
# 使用gdb加載main
(gdb) gdb main
# 在進入gdb控制臺后,在simulateDataChanged方法上增加斷點
(gdb) b main.cc:simulateDataChanged
# 運行程序
(gdb) r
# 連續(xù)執(zhí)行兩次下一步,使程序執(zhí)行到employee對象創(chuàng)建完成后
(gdb) n
(gdb) n
復制代碼
之后,我們對employee.name屬性進行監(jiān)控,只要name屬性的值發(fā)生了變化,就會觸發(fā)GDB中斷
# 監(jiān)視employee.name變量對應的地址數(shù)據(jù)
(gdb) watch -location employee.name
# 繼續(xù)執(zhí)行
(gdb) c
# 在觸發(fā)watch中斷后,查看中斷所在位置的堆棧
(gdb) bt
#直接跳轉(zhuǎn)到我們的代碼所處的棧幀
(gdb) f 1
復制代碼
在觸發(fā)中斷后,我們發(fā)現(xiàn)是中斷位置是在modify_func方法中。正是這個方法,在內(nèi)部修改了employee的name屬性。至此調(diào)查完畢。
5.5. 堆內(nèi)存重復釋放問題排查
堆內(nèi)存的重復釋放,會導致內(nèi)存泄露,被破壞的內(nèi)存可以被攻擊者利用,從而產(chǎn)生更為嚴重的安全問題。目標流行的C函數(shù)庫(比如libc),會在內(nèi)存重復釋放時,拋出“double free or corruption (fasttop)”錯誤,并終止程序運行。為了修復堆內(nèi)存重復釋放問題,我們需要找到所有釋放對應堆內(nèi)存的代碼位置,用來判斷哪一個釋放堆內(nèi)存的操作是不正確的。
使用GDB可以解決我們知道哪一個變量產(chǎn)生了內(nèi)存重復釋放,但我們不知道都在哪里對此變量釋放了內(nèi)存空間的問題。如果我們對產(chǎn)生內(nèi)存重復釋放問題的變量一無所知,那么還需要借助其它的工具來輔助定位。
下面我們使用兩個線程,在其中釋放同一塊堆內(nèi)存,用來模擬堆內(nèi)存重復釋放問題
void free1_func(Employee* employee) {
auto tid = gettid();
std::cout << "thread " << tid << " started" << std::endl;
employee->name = "new employee name1";
delete employee;
}
void free2_func(Employee* employee) {
auto tid = gettid();
std::cout << "thread " << tid << " started" << std::endl;
employee->name = "new employee name2";
delete employee;
}
void simulateDoubleFree() {
Employee *employee = new Employee("origin employee name");
std::thread thread1(free1_func, employee);
std::thread thread2(free2_func, employee);
thread1.join();
thread2.join();
}
復制代碼
編譯程序并運行,程序會因為employee變量的double free問題而終止
現(xiàn)在我們使用GDB來找到所有釋放employee變量堆內(nèi)存的代碼的位置,以便決定那個釋放操作是不需要的
# 使用GDB加載程序
gdb main
# 在employee變量創(chuàng)建完成后的位置設置斷點
(gdb) b main.cc:101
# 運行程序
(gdb) r
復制代碼
在程序中斷后,我們打印employee變量的堆內(nèi)存地址,并在所有釋放此內(nèi)存地址的位置添加條件斷點之后繼續(xù)執(zhí)行程序
# 查看employee變量
(gdb) p employee //$1 = (Employee *) 0x5555555712e0
# 在釋放employee變量時,增加條件斷點
(gdb) b __GI___libc_free if mem == 0x5555555712e0
# 繼續(xù)運行程序
(gdb) c
復制代碼
在程序中斷時,我們找到了釋放employee變量堆內(nèi)存的第一個位置,位于main.cc文件89行的delete employee操作。繼續(xù)執(zhí)行程序,我們會找到另一處釋放了employee堆內(nèi)存的代碼的位置。至此,我們已經(jīng)可以調(diào)整代碼來修復此double free問題
6. 常用的GDB命令
總結(jié)
GDB是探查查詢運行中各種疑難問題的利器。在實際應用中,問題產(chǎn)生的原因通常要復雜得多。程序可能在標準庫中產(chǎn)生了Crash,整個堆??赡芏际菢藴蕩齑a;程序可能由于我們的代碼的操作,最終在三方中間件中產(chǎn)生了問題;整個異常堆棧可能都不包含我們自己開發(fā)的代碼;面對被三方庫不知以何種方式使用的變量。我們除了需要熟悉GDB的使用之外,在這些復雜的實際問題上,我們還需要盡可能多地了解我們使用的其它庫的機制和原理。
最后:?下方這份完整的軟件測試視頻學習教程已經(jīng)整理上傳完成,朋友們?nèi)绻枰梢宰孕忻赓M領取【保證100%免費】
這些資料,對于【軟件測試】的朋友來說應該是最全面最完整的備戰(zhàn)倉庫,這個倉庫也陪伴上萬個測試工程師們走過最艱難的路程,希望也能幫助到你!文章來源:http://www.zghlxwxcb.cn/news/detail-764847.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-764847.html
到了這里,關于程序調(diào)試利器——GDB使用指南的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!