Valgrind
Valgrind 原理
valgrind 是一個提供了一些 debug 和優(yōu)化的工具的工具箱糖儡,可以使得你的程序減少內(nèi)存泄漏或者錯誤訪問.
valgrind 默認(rèn)使用 memcheck 去檢查內(nèi)存問題.
memcheck 檢測內(nèi)存問題的原理如下圖所示:
Memcheck 能夠檢測出內(nèi)存問題柴钻,關(guān)鍵在于其建立了兩個全局表碉怔。
- valid-value map:
對于進程的整個地址空間中的每一個字節(jié)(byte)卫枝,都有與之對應(yīng)的 8 個 bits拖叙;對于 CPU 的每個寄存器盅弛,也有一個與之對應(yīng)的 bit 向量。這些 bits 負責(zé)記錄該字節(jié)或者寄存器值是否具有有效的夺艰、已初始化的值。 - 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 來看一下崩潰處的錯誤吧
然后可以發(fā)現(xiàn)是 malloc 函數(shù)里面崩潰了氓辣,這看不出什么東西,再繼續(xù)查看一下函數(shù)調(diào)用的棧
然后我們就發(fā)現(xiàn)了其實是 jansson 庫的 new 的時候的 bug袱蚓,然后 debug 就自然而然的跑偏了
在這個時候就可以使用 valgrind 了:
valgrind --leak-check=yes ./p2p/test/addressbook_test
在這個打印出來的信息我們可以看到钞啸,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;
}