valgrind 的使用

Valgrind

Valgrind 原理

valgrind 是一個提供了一些 debug 和優(yōu)化的工具的工具箱糖儡,可以使得你的程序減少內(nèi)存泄漏或者錯誤訪問.
valgrind 默認(rèn)使用 memcheck 去檢查內(nèi)存問題.

memcheck 檢測內(nèi)存問題的原理如下圖所示:


valgrind.jpg

Memcheck 能夠檢測出內(nèi)存問題柴钻,關(guān)鍵在于其建立了兩個全局表碉怔。

  1. valid-value map:
    對于進程的整個地址空間中的每一個字節(jié)(byte)卫枝,都有與之對應(yīng)的 8 個 bits拖叙;對于 CPU 的每個寄存器盅弛,也有一個與之對應(yīng)的 bit 向量。這些 bits 負責(zé)記錄該字節(jié)或者寄存器值是否具有有效的夺艰、已初始化的值。
  2. valid-address map
    對于進程整個地址空間中的每一個字節(jié)(byte)沉衣,還有與之對應(yīng)的 1 個 bit郁副,負責(zé)記錄該地址是否能夠被讀寫。

檢測原理:

  • 當(dāng)要讀寫內(nèi)存中某個字節(jié)時豌习,首先檢查 valid-address map 中這個字節(jié)對應(yīng)的 A bit存谎。如果該A bit顯示該位置是無效位置,memcheck 則報告讀寫錯誤肥隆。
  • 內(nèi)核(core)類似于一個虛擬的 CPU 環(huán)境既荚,這樣當(dāng)內(nèi)存中的某個字節(jié)被加載到真實的 CPU 中時,該字節(jié)對應(yīng)的 V bit (在 valid-value map 中) 也被加載到虛擬的 CPU 環(huán)境中栋艳。一旦寄存器中的值恰聘,被用來產(chǎn)生內(nèi)存地址,或者該值能夠影響程序輸出吸占,則 memcheck 會檢查對應(yīng)的 V bits憨琳,如果該值尚未初始化,則會報告使用未初始化內(nèi)存錯誤旬昭。

Quick start

使用valgrind 很簡單篙螟, 首先編譯好要測試的程序 (為了使valgrind發(fā)現(xiàn)的錯誤更精確,如能夠定位到源代碼行问拘,建議在編譯時加上-g參數(shù)遍略,編譯優(yōu)化選項請選擇O0,雖然這會降低程序的執(zhí)行效率骤坐。)绪杏, 假設(shè)運行這個程序的命令是

./a.out arg1 arg2

那么要使用 valgrind 的話只需要運行

valgrind --leak-check=yes ./a.out arg1 arg2

就可以了.

valgrind的輸出也很好看得懂, 例如下面這個 C 程序

  #include <stdlib.h>

  void f(void)
  {
     int* x = malloc(10 * sizeof(int));
     x[10] = 0;        // problem 1: heap block overrun
  }                    // problem 2: memory leak -- x not freed

  int main(void)
  {
     f();
     return 0;
  }

valgrind 的輸出為

liu@liu ~> valgrind --leak-check=yes ./a.out 
==4372== Memcheck, a memory error detector
==4372== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==4372== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==4372== Command: ./a.out
==4372== 
==4372== Invalid write of size 4
==4372==    at 0x400504: f (in /home/liu/a.out)
==4372==    by 0x400523: main (in /home/liu/a.out)
==4372==  Address 0x51fa068 is 0 bytes after a block of size 40 alloc'd
==4372==    at 0x4C2EBAB: malloc (vg_replace_malloc.c:299)
==4372==    by 0x4004F7: f (in /home/liu/a.out)
==4372==    by 0x400523: main (in /home/liu/a.out)
==4372== 
==4372== 
==4372== HEAP SUMMARY:
==4372==     in use at exit: 40 bytes in 1 blocks
==4372==   total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==4372== 
==4372== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==4372==    at 0x4C2EBAB: malloc (vg_replace_malloc.c:299)
==4372==    by 0x4004F7: f (in /home/liu/a.out)
==4372==    by 0x400523: main (in /home/liu/a.out)
==4372== 
==4372== LEAK SUMMARY:
==4372==    definitely lost: 40 bytes in 1 blocks
==4372==    indirectly lost: 0 bytes in 0 blocks
==4372==      possibly lost: 0 bytes in 0 blocks
==4372==    still reachable: 0 bytes in 0 blocks
==4372==         suppressed: 0 bytes in 0 blocks
==4372== 
==4372== For counts of detected and suppressed errors, rerun with: -v
==4372== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

  • 如果一個函數(shù)內(nèi)部出現(xiàn)了內(nèi)存的訪問錯誤或者是沒有釋放纽绍,那么多次調(diào)用這個函數(shù)蕾久,valgrind 會多次輸出這個錯誤。所以當(dāng)valgrind 報出大量錯誤的時候拌夏,不要慌張僧著,其實可能只是很少一部份要改
  • 每一行開頭的數(shù)字,比如這里的 4372 顯示的是進程 id 障簿, 這個通常情況下是不需要看的
  • ==4372== Invalid write of size 4 這一行顯示這里有一個錯誤盹愚,就是 x 分配了 10 byte 的空間,但是向第 11 個 byte 寫數(shù)據(jù), 所以就會顯示 Invalid write 的錯誤
  • 在下面的這幾行顯示的是錯誤出現(xiàn)的位置站故, 因為是 stack trace皆怕, 所以需要先從最下面一行開始看
  • 內(nèi)存泄漏顯示的是如下的信息
    ==4372== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
    ==4372==    at 0x4C2EBAB: malloc (vg_replace_malloc.c:299)
    ==4372==    by 0x4004F7: f (in /home/liu/a.out)
    ==4372==    by 0x400523: main (in /home/liu/a.out)
    

Valgrind 分析常見的內(nèi)存問題

  • 使用未初始化的內(nèi)存

    下面代碼中 a[2] 沒有初始化

    #include <stdlib.h>
    #include <stdio.h>
    
    int main(void)
    {
        int a[5];
        a[0] = a[1] = a[3] = a[4]  = 0;
        int s = 0;
        for(int i=0;i<3;i++){
              s+=a[i];
        }
        printf("%d\n",s);
        return 0;
    }
    

    valgrind 顯示程序跳轉(zhuǎn)依賴于未初始化的變量:

    ==7007== Conditional jump or move depends on uninitialised value(s)
    ==7007==    at 0x4E8B4A9: vfprintf (in /usr/lib64/libc-2.27.so)
    ==7007==    by 0x4E935F5: printf (in /usr/lib64/libc-2.27.so)
    ==7007==    by 0x400540: main (test.c:12)
    ==7007== 
    ==7007== Use of uninitialised value of size 8
    ==7007==    at 0x4E8792E: _itoa_word (in /usr/lib64/libc-2.27.so)
    ==7007==    by 0x4E8B225: vfprintf (in /usr/lib64/libc-2.27.so)
    ==7007==    by 0x4E935F5: printf (in /usr/lib64/libc-2.27.so)
    ==7007==    by 0x400540: main (test.c:12)
    
  • 內(nèi)存讀寫越界

    具體例子就像是 上一節(jié) quick start 中的例子

  • 動態(tài)內(nèi)存管理錯誤

    • 申請和釋放不一致
      由于 C++ 兼容 C,而 C 與 C++ 的內(nèi)存申請和釋放函數(shù)是不同的,因此在 C++ 程序中愈腾,就有兩套動態(tài)內(nèi)存管理函數(shù)憋活。一條不變的規(guī)則就是采用 C 方式申請的內(nèi)存就用 C 方式釋放;用 C++ 方式申請的內(nèi)存虱黄,用 C++ 方式釋放悦即。也就是用 malloc/alloc/realloc 方式申請的內(nèi)存,用 free 釋放礁鲁;用 new 方式申請的內(nèi)存用 delete 釋放。在上述程序中赁豆,用 malloc 方式申請了內(nèi)存卻用 delete 來釋放仅醇,雖然這在很多情況下不會有問題,但這絕對是潛在的問題魔种。
    • 申請和釋放不匹配
      申請了多少內(nèi)存析二,在使用完成后就要釋放多少。如果沒有釋放节预,或者少釋放了就是內(nèi)存泄露叶摄;多釋放了也會產(chǎn)生問題。
    • 釋放后仍然讀寫
      本質(zhì)上說安拟,系統(tǒng)會在堆上維護一個動態(tài)內(nèi)存鏈表蛤吓,如果被釋放,就意味著該塊內(nèi)存可以繼續(xù)被分配給其他部分糠赦,如果內(nèi)存被釋放后再訪問会傲,就可能覆蓋其他部分的信息,這是一種嚴(yán)重的錯誤拙泽。

    例子

    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    #include <malloc.h>
    
    int main(void)
    {
        int *a = (int*)malloc(sizeof(int)*3);
        a[0]=1;
        a[1]=2;
        a[2]=3;
        free(a);
        printf("%d\n", a[0]);  // invalid read
        a[0]=1;    // invalid write
        free(a);  // free a two times
        return 0;
    }
    
    ==7556== Memcheck, a memory error detector
    ==7556== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
    ==7556== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
    ==7556== Command: ./a.out
    ==7556== 
    ==7556== Invalid read of size 4
    ==7556==    at 0x4005C2: main (test.c:13)
    ==7556==  Address 0x51fa040 is 0 bytes inside a block of size 12 free'd
    ==7556==    at 0x4C2FDAC: free (vg_replace_malloc.c:530)
    ==7556==    by 0x4005BD: main (test.c:12)
    ==7556==  Block was alloc'd at
    ==7556==    at 0x4C2EBAB: malloc (vg_replace_malloc.c:299)
    ==7556==    by 0x400587: main (test.c:8)
    ==7556== 
    1
    ==7556== Invalid write of size 4
    ==7556==    at 0x4005D9: main (test.c:14)
    ==7556==  Address 0x51fa040 is 0 bytes inside a block of size 12 free'd
    ==7556==    at 0x4C2FDAC: free (vg_replace_malloc.c:530)
    ==7556==    by 0x4005BD: main (test.c:12)
    ==7556==  Block was alloc'd at
    ==7556==    at 0x4C2EBAB: malloc (vg_replace_malloc.c:299)
    ==7556==    by 0x400587: main (test.c:8)
    ==7556== 
    ==7556== Invalid free() / delete / delete[] / realloc()
    ==7556==    at 0x4C2FDAC: free (vg_replace_malloc.c:530)
    ==7556==    by 0x4005EA: main (test.c:15)
    ==7556==  Address 0x51fa040 is 0 bytes inside a block of size 12 free'd
    ==7556==    at 0x4C2FDAC: free (vg_replace_malloc.c:530)
    ==7556==    by 0x4005BD: main (test.c:12)
    ==7556==  Block was alloc'd at
    ==7556==    at 0x4C2EBAB: malloc (vg_replace_malloc.c:299)
    ==7556==    by 0x400587: main (test.c:8)
    ==7556== 
    ==7556== 
    ==7556== HEAP SUMMARY:
    ==7556==     in use at exit: 0 bytes in 0 blocks
    ==7556==   total heap usage: 2 allocs, 3 frees, 1,036 bytes allocated
    ==7556== 
    ==7556== All heap blocks were freed -- no leaks are possible
    ==7556== 
    ==7556== For counts of detected and suppressed errors, rerun with: -v
    ==7556== ERROR SUMMARY: 3 errors from 3 contexts (suppressed: 0 from 0)
    
    

Used in TM

在 debug 的過程中淌山,gdb 和程序崩潰的時候顯示的信息都具有一定的誤導(dǎo)性,但是valgrind 對于查找bug的幫助很大, 按照程序的邏輯顾瞻,每秒種將 struct 結(jié)構(gòu)序列化成 json 格式并保存到文件中去(保存一次打印一個 tick )泼疑,然后十秒之后終止。但是我們可以看到荷荤,這里第三次打印的時候就崩潰了退渗。系統(tǒng)提示是(address boundary error)。

[圖片上傳中...(Selection_001.png-27236b-1557573570699-0)]

講道理蕴纳,我們現(xiàn)在應(yīng)該使用 gdb 來看一下崩潰處的錯誤吧

Selection_002.png

然后可以發(fā)現(xiàn)是 malloc 函數(shù)里面崩潰了氓辣,這看不出什么東西,再繼續(xù)查看一下函數(shù)調(diào)用的棧

Selection_003.png

然后我們就發(fā)現(xiàn)了其實是 jansson 庫的 new 的時候的 bug袱蚓,然后 debug 就自然而然的跑偏了

在這個時候就可以使用 valgrind 了:

valgrind --leak-check=yes ./p2p/test/addressbook_test
Selection_004.png

在這個打印出來的信息我們可以看到钞啸,json_decref invalid read 了內(nèi)存了,說明 json_decref 的參數(shù)(一個 json_t 的對象)已經(jīng)提前釋放了。
然后根據(jù)提示体斩,找到了 pear_address_book.c:470 行的代碼:

pr_save_json_to_file(json, file_path);
json_decref(json);
g_ptr_array_unref(addr_book_json->addrs);

然后進去 pr_save_json_to_file(json, file_path) 里面看了一下梭稚,里面已經(jīng) 對 json 對象 調(diào)用了 json_decref 了,所以有兩次釋放絮吵。

valgrind 的一些問題

對于下列的代碼弧烤,valgrind 就會報出內(nèi)存內(nèi)存泄漏, 但是將代碼 g_ptr_array_free(b,0); 改成 g_ptr_array_unref(b); 就完全沒問題,雖然沒有設(shè)定釋放函數(shù)蹬敲,也沒有釋放暇昂。

#include <glib.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
        GPtrArray* a = g_ptr_array_new_with_free_func(g_free);
        GPtrArray* b = g_ptr_array_new();
        for(int i=0;i<5;i++){
                int * t = g_new(int,1);
                g_ptr_array_add(a, t);
                g_ptr_array_add(b, t);
        }
        g_ptr_array_unref(a);
        g_ptr_array_free(b,0);
        return 0;
}

Selection_005.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市伴嗡,隨后出現(xiàn)的幾起案子急波,更是在濱河造成了極大的恐慌,老刑警劉巖瘪校,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件澄暮,死亡現(xiàn)場離奇詭異,居然都是意外死亡阱扬,警方通過查閱死者的電腦和手機泣懊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來麻惶,“玉大人馍刮,你說我怎么就攤上這事∏蕴#” “怎么了渠退?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長脐彩。 經(jīng)常有香客問我碎乃,道長,這世上最難降的妖魔是什么惠奸? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任梅誓,我火速辦了婚禮,結(jié)果婚禮上佛南,老公的妹妹穿的比我還像新娘梗掰。我一直安慰自己,他們只是感情好嗅回,可當(dāng)我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布及穗。 她就那樣靜靜地躺著,像睡著了一般绵载。 火紅的嫁衣襯著肌膚如雪埂陆。 梳的紋絲不亂的頭發(fā)上苛白,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天,我揣著相機與錄音焚虱,去河邊找鬼购裙。 笑死,一個胖子當(dāng)著我的面吹牛鹃栽,可吹牛的內(nèi)容都是我干的躏率。 我是一名探鬼主播,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼民鼓,長吁一口氣:“原來是場噩夢啊……” “哼薇芝!你這毒婦竟也來了空镜?” 一聲冷哼從身側(cè)響起咪啡,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎树灶,沒想到半個月后供嚎,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體黄娘,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡峭状,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年克滴,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片优床。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡劝赔,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出胆敞,到底是詐尸還是另有隱情着帽,我是刑警寧澤,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布移层,位于F島的核電站仍翰,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏观话。R本人自食惡果不足惜予借,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望频蛔。 院中可真熱鬧灵迫,春花似錦、人聲如沸晦溪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽三圆。三九已至狞换,卻和暖如春避咆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背哀澈。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工牌借, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人割按。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓膨报,卻偏偏與公主長得像,于是被迫代替她去往敵國和親适荣。 傳聞我的和親對象是個殘疾皇子现柠,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容