一、mtrace分析內存泄露
mtrace(memory trace),是 GNU Glibc 自帶的內存問題檢測工具,它可以用來協助定位內存泄露問題。 它的實現源碼在glibc源碼的malloc目錄下,其基本設計原理為設計一個函數 void mtrace (),函數對 libc 庫中的 malloc/free 等函數的調用進行追蹤,由此來檢測內存是否存在泄漏的情況。mtrace是一個C函數,在
voidmtrace(void);
mtrace原理
mtrace()函數會為那些和動態內存分配有關的函數(譬如 malloc()、realloc()、memalign() 以及 free())安裝 “鉤子(hook)” 函數,這些 hook 函數會為我們記錄所有有關內存分配和釋放的跟蹤信息,而 muntrace() 則會卸載相應的 hook 函數。
基于這些 hook 函數生成的調試跟蹤信息,我們就可以分析是否存在 “內存泄露” 這類問題了。
設置日志生成路徑
mtrace 機制需要我們實際運行一下程序,然后才能生成跟蹤的日志,但在實際運行程序之前還有一件要做的事情是需要告訴 mtrace (即前文提到的 hook 函數)生成日志文件的路徑。
設置日志生成路徑有兩種,一種是設置環境變量:export MALLOC_TRACE=./test.log // 當前目錄下另一種是在代碼層面設置:setenv("MALLOC_TRACE", "output_file_name", 1);``output_file_name就是儲存檢測結果的文件的名稱。
測試實例
#include從上述代碼中,我們希望能夠在程序開始到結束檢查內存是否泄漏的問題,例子簡單,一眼就能看出存在內存泄漏的問題,所以我們需要驗證 mtrace 是否能夠檢查出來內存泄漏問題,且檢查的結果如何分析定位。gcc -g test.c -o test生成可執行文件。#include #include intmain(intargc,char**argv) { mtrace();//開始跟蹤 char*p=(char*)malloc(100); free(p); p=NULL; p=(char*)malloc(100); muntrace();//結束跟蹤,并生成日志信息 return0; }
日志
程序運行結束,會在當前目錄生成 test.log 文件,打開可以看到一下內容:
=Start @./test:[0x400624]+0x21ed4500x64 @./test:[0x400634]-0x21ed450 @./test:[0x400646]+0x21ed4500x64 =End從這個文件中可以看出中間三行分別對應源碼中的 malloc -> free -> malloc 操作。解讀:./test 指的是我們執行的程序名字,[0x400624] 是第一次調用 malloc 函數機器碼中的地址信息,+ 表示申請內存( - 表示釋放),0x21ed450 是 malloc 函數申請到的地址信息,0x64 表示的是申請的內存大小。 由此分析第一次申請已經釋放,第二次申請沒有釋放,存在內存泄漏的問題。
泄露分析
使用addr2line工具定位源碼位置
通過使用 "addr2line" 命令工具,得到源文件的行數(通過這個可以根據機器碼地址定位到具體源碼位置)
#addr2line-etest0x400624 /home/test.c:9
使用mtrace工具分析日志信息
mtrace + 可執行文件路徑 + 日志文件路徑mtrace test ./test.log執行,輸出如下信息:
Memorynotfreed: ----------------- AddressSizeCaller 0x00000000021ed4500x64at/home/test.c:14
二、Valgrind分析內存泄露
Valgrind工具介紹
Valgrind是一套Linux下,開放源代碼(GPL V2)的仿真調試工具的集合。Valgrind由內核(core)以及基于內核的其他調試工具組成。 內核類似于一個框架(framework),它模擬了一個CPU環境,并提供服務給其他工具;而其他工具則類似于插件 (plug-in),利用內核提供的服務完成各種特定的內存調試任務。Valgrind的體系結構如下圖所示
1、Memcheck
最常用的工具,用來檢測程序中出現的內存問題,所有對內存的讀寫都會被檢測到,一切對malloc() / free() / new / delete 的調用都會被捕獲。
所以,它能檢測以下問題:對未初始化內存的使用;讀/寫釋放后的內存塊;讀/寫超出malloc分配的內存塊;讀/寫不適當的棧中內存塊;內存泄漏,指向一塊內存的指針永遠丟失;不正確的malloc/free或new/delete匹配;memcpy()相關函數中的dst和src指針重疊。
2、Callgrind
和 gprof 類似的分析工具,但它對程序的運行觀察更是入微,能給我們提供更多的信息。和 gprof 不同,它不需要在編譯源代碼時附加特殊選項,但加上調試選項是推薦的。
Callgrind 收集程序運行時的一些數據,建立函數調用關系圖,還可以有選擇地進行 cache 模擬。在運行結束時,它會把分析數據寫入一個文件。callgrind_annotate 可以把這個文件的內容轉化成可讀的形式。
3、Cachegrind
Cache 分析器,它模擬 CPU 中的一級緩存 I1,Dl 和二級緩存,能夠精確地指出程序中 cache 的丟失和命中。如果需要,它還能夠為我們提供 cache 丟失次數,內存引用次數,以及每行代碼,每個函數,每個模塊,整個程序產生的指令數。這對優化程序有很大的幫助。
4、Helgrind
它主要用來檢查多線程程序中出現的競爭問題。Helgrind 尋找內存中被多個線程訪問,而又沒有一貫加鎖的區域,這些區域往往是線程之間失去同步的地方,而且會導致難以發掘的錯誤。
Helgrind 實現了名為“Eraser”的競爭檢測算法,并做了進一步改進,減少了報告錯誤的次數。不過,Helgrind 仍然處于實驗階段。
5、Massif
堆棧分析器,它能測量程序在堆棧中使用了多少內存,告訴我們堆塊,堆管理塊和棧的大小。
Massif 能幫助我們減少內存的使用,在帶有虛擬內存的現代系統中,它還能夠加速我們程序的運行,減少程序停留在交換區中的幾率。
此外,lackey 和 nulgrind 也會提供。Lackey 是小型工具,很少用到;Nulgrind 只是為開發者展示如何創建一個工具。
Memcheck原理
本文的重點是在檢測內存泄露,所以對于valgrind的其他工具不做過多的說明,主要說明下Memcheck的工作。Memcheck檢測內存問題的原理如下圖所示:
Memcheck 能夠檢測出內存問題,關鍵在于其建立了兩個全局表。
Valid-Value 表 對于進程整個地址空間中的每一個字節(byte),都有與之對應的 8個bits;對于 CPU 的每個寄存器,也有一個與之對應的 bit 向量。這些 bits 負責記錄該字節或者寄存器值是否具有有效的、已初始化的值。
Valid-Address 表 對于進程整個地址空間中的每一個字節(byte),還有與之對應的1個 bit,負責記錄該地址是否能夠被讀寫。
檢測原理:當要讀寫內存中某個字節時,首先檢查這個字節對應的Valid-Address 表中的 A bit。如果該 A bit顯示該位置是無效位置,memcheck 則報告讀寫錯誤。內核(core)類似于一個虛擬的 CPU 環境,這樣當內存中的某個字節被加載到真實的 CPU 中時,該字節對應的Valid-Value 表中的 V bit 也被加載到虛擬的 CPU 環境中。一旦寄存器中的值,被用來產生內存地址,或者該值能夠影響程序輸出,則 memcheck 會檢查對應的V bits,如果該值尚未初始化,則會報告使用未初始化內存錯誤。
內存泄露類型
valgrind 將內存泄漏分成 4 類:
確立泄露(definitely lost):運行內存還沒有釋放出來,但早已沒有表針偏向運行內存,運行內存早已不能瀏覽。確立泄露的運行內存是強烈要求修補的。
間接性泄露(indirectly lost):泄露的運行內存表針儲存在確立泄露的運行內存中,伴隨著確立泄露的運行內存不能瀏覽,造成間接性泄露的運行內存也不能瀏覽。例如:
structlist{ structlist*next; }; intmain(intargc,char**argv) { structlist*root; root=(structlist*)malloc(sizeof(structlist)); root->next=(structlist*)malloc(sizeof(structlist)); printf("root%proop->next%p ",root,root->next); root=NULL; return0; }這里遺失的是 root 表針(是確立泄露類型),造成 root 儲存的 next 表針變成了間接性泄露。間接性泄露的運行內存毫無疑問也要修補的,但是一般會伴隨著 確立泄露 的修補而修補。
很有可能泄露(possibly lost):表針并不偏向運行內存頭詳細地址,只是偏向運行內存內部的部位。valgrind 往往會猜疑很有可能泄露,是由于表針早已偏位,并沒有偏向運行內存頭,只是有運行內存偏位,偏向運行內存內部的部位。有一些情況下,這并并不是泄露,由于這種程序流程便是那么設計方案的,比如為了更好地完成內存對齊,附加申請辦理運行內存,回到兩端對齊后的內存地址。
仍可訪達(still reachable):表針一直存有且偏向運行內存頭頂部,直到程序流程撤出時運行內存還沒有釋放出來。
Valgrind參數設置
--leak-check=
--log-fd=[default: 2, stderr] valgrind 打印日志轉存到指定文件或者文件描述符。如果沒有這個參數,valgrind 的日志會連同用戶程序的日志一起輸出,會顯得非常亂。
--trace-children=
--keep-debuginfo=
--keep-stacktraces=
--freelist-vol=當客戶程序用 free 或 delete 釋放一個內存塊時,這個內存塊不會立即可用于再分配,它只會被放在一個freed blocks的隊列中(freelist)并被標記為不可訪問,這樣有利于探測到在一段很重要的時間后,客戶程序又對被釋放的塊進行訪問的錯誤。這個選項規定了隊列所占的字節塊大小,默認是20MB。增大這個選項的會增大memcheck的內存開銷,但查這類錯的能力也會提升。
--freelist-big-blocks=當從 freelist 隊列中取可用內存塊用于再分配時,memcheck 將會從那些比 number 大的內存塊中按優先級取出一個塊出來用。這個選項就防止了 freelist 中那些小的內存塊的頻繁調用,這個選項提高了 查到針對小內存塊的野指針錯誤的幾率。若這個選項設為0,則所有的塊將按先進先出的原則用于再分配。默認是1M。參考:valgrind 簡介(內存檢查工具)
編譯參數推薦
為了更好地在出難題時要詳盡打印出出去棧信息內容,實際上大家最好是在編譯程序時加上 -g 選擇項。如果有動態性載入的庫,必須再加上--keep-debuginfo=yes,不然假如發覺是動態性載入的庫發生泄露,因為動態庫被卸載掉了,造成找不到符號表。編碼編譯程序提升,不建議應用 -O2既之上。-O0很有可能會造成運作變慢,建議使用-O1。
檢測實例說明
申請不釋放內存
#include#include voidfunc() { //只申請內存而不釋放 void*p=malloc(sizeof(int)); } intmain() { func(); return0; }
使用valgrind命令來執行程序同時輸出日志到文件
valgrind--log-file=valReport--leak-check=full--show-reachable=yes--leak-resolution=low./a.out參數說明:
–log-file=valReport 是指定生成分析日志文件到當前執行目錄中,文件名為valReport
–leak-check=full 顯示每個泄露的詳細信息
–show-reachable=yes 是否檢測控制范圍之外的泄漏,比如全局指針、static指針等,顯示所有的內存泄露類型
–leak-resolution=low 內存泄漏報告合并等級
–track-origins=yes表示開啟“使用未初始化的內存”的檢測功能,并打開詳細結果。如果沒有這句話,默認也會做這方面的檢測,但不會打印詳細結果。執行輸出后,報告解讀,其中54017是指進程號,如果程序使用了多進程的方式來執行,那么就會顯示多個進程的內容。
==54017==Memcheck,amemoryerrordetector ==54017==Copyright(C)2002-2017,andGNUGPL'd,byJulianSewardetal. ==54017==UsingValgrind-3.15.0andLibVEX;rerunwith-hforcopyrightinfo ==54017==Command:./a.out ==54017==ParentPID:52130
第二段是對堆內存分配的總結信息,其中提到程序一共申請了1次內存,其中0次釋放了,4 bytes被分配(1 allocs, 0 frees, 4 bytes allocated)。
在head summary中,有該程序使用的總heap內存量,分配內存次數和釋放內存次數,如果分配內存次數和釋放內存次數不一致則說明有內存泄漏。
==54017==HEAPSUMMARY: ==54017==inuseatexit:4bytesin1blocks ==54017==totalheapusage:1allocs,0frees,4bytesallocated
第三段的內容描述了內存泄露的具體信息,其中有一塊內存占用4字節(4 bytes in 1 blocks),在調用malloc分配,調用棧中可以看到是func函數最后調用了malloc,所以這一個信息是比較準確的定位了我們泄露的內存是在哪里申請的。
==54017==4bytesin1blocksaredefinitelylostinlossrecord1of1 ==54017==at0x4C29F73:malloc(vg_replace_malloc.c:309) ==54017==by0x40057E:func()(in/home/oceanstar/CLionProjects/Share/src/a.out) ==54017==by0x40058D:main(in/home/oceanstar/CLionProjects/Share/src/a.out)
最后這一段是總結,4字節為一塊的內存泄露。
==54017==LEAKSUMMARY: ==54017==definitelylost:4bytesin1blocks//確立泄露 ==54017==indirectlylost:0bytesin0blocks//間接性泄露 ==54017==possiblylost:0bytesin0blocks//很有可能泄露 ==54017==stillreachable:0bytesin0blocks//仍可訪達 ==54017==suppressed:0bytesin0blocks
讀寫越界
#include#include intmain() { intlen=5; int*pt=(int*)malloc(len*sizeof(int));//problem1:notfreed int*p=pt; for(inti=0;i
problem1: 指針pt申請了空間,但是沒有釋放; problem2: pt申請了5個int的空間,p經過5次循環已達到p[5]的位置,*p = 5時,訪問越界(寫越界)。(下面valgrind報告中 Invalid write of size 4)
==58261==Invalidwriteofsize4 ==58261==at0x400707:main(main.cpp:12) ==58261==Address0x5a23054is0bytesafterablockofsize20alloc'd ==58261==at0x4C29F73:malloc(vg_replace_malloc.c:309) ==58261==by0x4006DC:main(main.cpp:7)
problem1: 讀越界 (下面valgrind報告中 Invalid read of size 4 )
==58261==Invalidreadofsize4 ==58261==at0x400711:main(main.cpp:13) ==58261==Address0x5a23054is0bytesafterablockofsize20alloc'd ==58261==at0x4C29F73:malloc(vg_replace_malloc.c:309) ==58261==by0x4006DC:main(main.cpp:7)
重復釋放
#include#include intmain() { int*x; x=static_cast (malloc(8*sizeof(int))); x=static_cast (malloc(8*sizeof(int))); free(x); free(x); return0; }
報告如下,Invalid free() / delete / delete[] / realloc()
==59602==Invalidfree()/delete/delete[]/realloc() ==59602==at0x4C2B06D:free(vg_replace_malloc.c:540) ==59602==by0x4006FE:main(main.cpp:10) ==59602==Address0x5a230a0is0bytesinsideablockofsize32free'd ==59602==at0x4C2B06D:free(vg_replace_malloc.c:540) ==59602==by0x4006F2:main(main.cpp:9) ==59602==Blockwasalloc'dat ==59602==at0x4C29F73:malloc(vg_replace_malloc.c:309) ==59602==by0x4006E2:main(main.cpp:8)
申請釋放接口不匹配
申請釋放接口不匹配的報告如下,用malloc申請空間的指針用free釋放;用new申請的空間用delete釋放(Mismatched free() / delete / delete []):
==61950==Mismatchedfree()/delete/delete[] ==61950==at0x4C2BB8F:operatordelete[](void*)(vg_replace_malloc.c:651) ==61950==by0x4006E8:main(main.cpp:8) ==61950==Address0x5a23040is0bytesinsideablockofsize5alloc'd ==61950==at0x4C29F73:malloc(vg_replace_malloc.c:309) ==61950==by0x4006D1:main(main.cpp:7)
內存覆蓋
intmain() { charstr[11]; for(inti=0;i11;?i++){ ????????str[i]?=?i; ????} ????memcpy(str?+?1,?str,?5); ????char?x[5]?=?"abcd"; ????strncpy(x?+?2,?x,?3); }
問題出在memcpy上, 將str指針位置開始copy 5個char到str+1所指空間,會造成內存覆蓋。strncpy也是同理。報告如下,Source and destination overlap:
==61609==Sourceanddestinationoverlapinmemcpy(0x1ffefffe31,0x1ffefffe30,5) ==61609==at0x4C2E81D:memcpy@@GLIBC_2.14(vg_replace_strmem.c:1035) ==61609==by0x400721:main(main.cpp:11) ==61609== ==61609==Sourceanddestinationoverlapinstrncpy(0x1ffefffe25,0x1ffefffe23,3) ==61609==at0x4C2D453:strncpy(vg_replace_strmem.c:552) ==61609==by0x400748:main(main.cpp:14)
三、總結
內存檢測方式無非分為兩種:
1、維護一個內存操作鏈表,當有內存申請操作時,將其加入此鏈表中,當有釋放操作時,從申請操作從鏈表中移除。如果到程序結束后此鏈表中還有內容,說明有內存泄露了;如果要釋放的內存操作沒有在鏈表中找到對應操作,則說明是釋放了多次。使用此方法的有內置的調試工具,Visual Leak Detecter,mtrace, memwatch, debug_new。
2、模擬進程的地址空間。仿照操作系統對進程內存操作的處理,在用戶態下維護一個地址空間映射,此方法要求對進程地址空間的處理有較深的理解。因為Windows的進程地址空間分布不是開源的,所以模擬起來很困難,因此只支持Linux。采用此方法的是 valgrind。
審核編輯:劉清
-
寄存器
+關注
關注
31文章
5225瀏覽量
118947 -
LINUX內核
+關注
關注
1文章
315瀏覽量
21544 -
GNU
+關注
關注
0文章
142瀏覽量
17391 -
cache技術
+關注
關注
0文章
41瀏覽量
1029 -
gpl
+關注
關注
0文章
26瀏覽量
2150
原文標題:Linux 內存泄漏檢測的基本方法
文章出處:【微信號:嵌入式開發愛好者,微信公眾號:嵌入式開發愛好者】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論