1. 程式人生 > >C++霧中風景番外篇3:GDB與Valgrind ,除錯程式碼記憶體的工具

C++霧中風景番外篇3:GDB與Valgrind ,除錯程式碼記憶體的工具

寫 C++的同學想必有太多和記憶體打交道的血淚經驗了,常常被 C++的記憶體問題攪的焦頭爛額。(寫 core 的經驗了)有很多同學一見到 core 就兩眼一抹黑,不知所措了。筆者 入""C++之後,在除錯 C++程式碼的過程之中,學習了不少除錯程式碼記憶體的工具。希望借這個機會來介紹一下筆者常用的工具,GDB,Valgrind等等,相信大家通過好好運用這些工具,能更好的馴服記憶體這匹"野馬"。

1.利用 GDB 除錯 CoreDump

CoreDump時一個二進位制的檔案,程序發生錯誤崩潰時,核心會產生一個瞬時的快照,記錄該程序的記憶體、執行堆疊狀態等資訊儲存在core檔案之中。做個簡單的類比,core 檔案相當於飛機執行時的"黑匣子

",能夠幫助我們更好的除錯 C++程式的問題。OK,接下來筆者將介紹一下如果利用GDB 來除錯 CoreDump的檔案。

  • CoreDump 檔案的大小

首先我們先確定一下作業系統是否會產生 CoreDump 檔案。通過ulimit -c獲取 core 檔案的限制大小:
檢視 core 檔案的大小限制

上面顯示筆者電腦的 core 檔案的大小是0,我們需要調整一下。通過ulimit調整為無限制。當然這種調整是臨時的,reboot 之後就恢復為0了。

ulimit -c ulimited

如果需要永久修改,可以通過/etc/security/limits.conf 來修改 core 檔案的大小。

  • CoreDump 檔案的生成路徑
    預設情況下,core dump生成的檔名為core,而且就在程式當前目錄下。通過修改/proc/sys/kernel/core_pattern可以控制core檔案儲存位置和檔案格式。(建議將字尾改為程序號)
    筆者這裡簡單起見,不進行修改了。
  • 編寫core 程式碼,這裡筆者利用執行緒訪問了空指標
#include <unistd.h>
#include <thread>

void core() {
    char* ch = nullptr;
    *ch = 'a';
}

int main() {
    auto t1 = std::thread(core);
    sleep(5);
    return 0;
}
  • 編譯執行該程式碼,產生段錯誤,生成了 core 檔案
    圖片.png
  • 利用 GDB 除錯 core 檔案
    除錯 core 檔案需要利用原生編譯出的二進位制檔案除錯。這裡有一點需要注意的,如果編譯 C++檔案之時沒有加-g
    的編譯選項,core 檔案的除錯內容會不夠完整。筆者這裡建議開啟對應的編譯選項,這會導致對應的二進位制檔案變大,編譯時間變長。(生產環境可以考慮關閉)使用gdb 二進位制檔案 core 檔案開啟 core 檔案。

利用 gdb 除錯 core 檔案

core 檔案列出了兩個執行緒的資訊。我們需要判斷對應的問題程式碼的定位,接下來我們一起來梳理一下:
info thread檢視執行緒的執行情況,在這裡我們就可以判斷程式碼 core 在什麼執行緒之中了,如果還是無法確定,可以通過thread apply all bt列出更加詳盡的堆疊資訊。

用 info thread 檢視執行緒執行情況
利用 thread apply all bt 顯示詳盡的堆疊資訊
通過上述資訊可以確認,thread 1的程式碼存在問題。我們通過thread 1切換到 thread 1,用bt顯示堆疊資訊繼續追查:
Thread 1的堆疊資訊

之後我們來看看令人生疑的棧內容,這裡顯然棧0是我們懷疑的程式碼,用frame 1檢視。
對應存在『問題』的語句

好了,這裡我們找到了引起問題罪魁禍首的程式碼,訪問了空指標。

小結

程式執行的 core 檔案是我們除錯程式碼十分重要依據,通過 GDB 可以很好的給出我們修改程式碼的線索和參考,熟悉掌握GDB 的除錯技巧,能夠大大解放我們除錯問題程式碼的生產力。

2.利用Valgrind判斷記憶體洩露

亡羊補牢不如未雨綢繆,與其等到出現程式崩潰時使用 GDB 來除錯解決,不如事前確認程式碼之中可能引發的問題。所以筆者接下來要介紹一款來自大不列顛的C++程式碼分析神器:Valgrind。(Valgrind的作者也通過開發Valgrind獲得了第二屆Google-O'Reilly開原始碼大獎~~~)
Valgrind 十分強大,適用於記憶體分析,洩漏檢測、鎖分析,效能評估。筆者也只掌握了一些基本的入門使用。希望這裡能夠拋磚引玉,更多複雜的用法煩請參考官方文件

Valgrind的安裝

Valgrind的安裝很簡單,筆者的發行版帶了對應的 deb 包。通過 apt-get 的包管理工具就可以直接安裝了,其他的發行版也可以作為參考。

sudo apt-get install valgrind
Valgrind的使用

與 GDB 類似,Valgrind 同樣推薦使用-g作為編譯引數。能夠更好的對程式碼進行分析。這裡我們依舊使用之前的例子進行測試:

valgrind ./untitiled

下面是 Valgrind 的分析結果:
valgrind 的分析結果

這裡有顯示Invalid write of size 1,說明這裡有一個不合法的寫入,並且寫入了1個位元組的內容。也就是指的是我們之前程式碼之中寫入空指標的行為。

接下來我們要展示 Valgrind更加強大的功能。它展示了程式的記憶體使用情況,並且給出總結:
valgrind 對記憶體的分析
這裡列出了多種的記憶體洩露情況:

  • definitely lost: 肯定的記憶體洩漏,這表示在程式退出時,有記憶體沒有回收,但是也沒有指標指向該記憶體。這種情況最為嚴重。
  • indirectly lost: 間接的記憶體洩漏,如類之中定義的指標指向的記憶體沒有回收。這種情況和上述相同。
  • possibly lost: 可能出現記憶體洩漏。這種情況需要仔細排查,可能程式碼沒有問題,也可能有異常的記憶體洩露。
  • still reachable: 程式沒主動釋放記憶體,在退出時候該記憶體仍能訪問到。這種情況一般問題不大,因為程式退出之後作業系統會回收程式的記憶體,所以這種情況一般問題不大。

這裡沒有給出具體洩露的內容,需要加入引數--leak-check=full將完整的結果打印出來,會指出對應的引起記憶體洩露的具體程式碼,可以繼續深入分析。

程式碼調優

這裡進行程式碼調優的時,需要利用qcachegrind來進行分析。首先筆者先進行安裝:

sudo apt-get install qcachegrind 

之後我們呼叫Valgrind來生成執行資料:

 valgrind --tool=callgrind -v main(需要分析的程式)

執行之後在目錄下生成對應的分析資料,我們用qcachegrind 開啟,這裡用的程式碼是筆者之前實現的 SkipList

qcachegrind callgrind.out.29235 

接下來我們來分析對應的結果:
valgrind 的分析結果

上圖顯示了各個函式的被呼叫的耗時百分比,我們可以選取對效能感興趣的函式來進行深入分析。我們下面繼續分析其中一個函式被呼叫和它使用函式的效能情況
insert 的函式被外呼叫的情況
insert 函式呼叫函式的情況與耗時分析

所以通過上述資料,我們可以給出效能分析的證據和線索,依據這些資訊來更好的優化我們程式碼的效能。

3.小結

本文介紹了亡羊補牢的工具 GDB,也簡介了未雨綢繆的Valgrind 。通過上述工具對C++程式更加深入分析。工欲善其事,必先利其器,希望大家也能好好掌握這些提供生產力的工具,讓 C++不再惱人