在實際的軟件開發過程中,內存問題常常是耗費大量時間進行分析的挑戰之一。為了更有效地定位和解決與內存相關的難題,一系列輔助工具應運而生,其中備受贊譽的Valgrind工具便是其中之一。事實上,筆者本人曾利用Valgrind工具成功地發現并解決了一個隱藏在軟件中的bug,這充分體現了工具在開發過程中的重要性。
然而,同樣強大的bpftrace工具同樣具備簡潔而直觀的特點,能夠協助我們高效地追蹤內存泄漏問題。在這方面,bpftrace提供了一種更加精細的、實時的分析方式,幫助開發人員準確地定位代碼中可能存在的內存泄漏情況。
構建樣例
我們編寫一個程序--mem_check.c,代碼中包含正確的申請內存和釋放內存的邏輯,同時包含存在內存泄露的代碼代碼。。
#include
int main(){
char *p1 = NULL;
char *p2 = NULL;
for(int i = 0; i < 5; i++)
{
p1 = malloc(16);
}
for(int i = 0; i < 5; i++)
{
p2 = malloc(32);
free(p2);
}
getchar();
return 0;
}
上面的代碼非常簡單,我們申請了5次16個字節的內存,沒有釋放,存在內存泄露。申請5次32個字節的內存,有釋放,沒存在內存泄露。那么我們如何通過bpftrace定位呢?
我們通過bpftrace對mem_check.c進行動態的統計內存的申請和釋放,定位內存泄露的問題。我們需要對關鍵的兩個接口進行probe--malloc和free,這兩個接口的實現在libc中。
編譯mem_check.c文件,生成可執行文件:
探測mem_ckeck可執行文件
bpftrace可以對內核態進行探測也可以對用戶態進行探測,其中探針如下:
- 內核態探針:kprobe/kretprobe
- 用戶態探針:uprobe/uretprobe
mem_check.c是一個應用程序,顯然我們需要使用用戶態探針:uprobe/uretprobe
通過uprobe探測mem_check.c中的malloc函數,我們單行指令驗證,參數格式是 uprobe:可執行文件:函數名:
理論是沒有沒有問題,但實際發生錯誤:No probes to attach。原因:可執行文件mem_check中找不到符號:malloc,我們可以通過nm命令確定一下:
我們發現malloc是一個鏈接自GLIBC_2.2.5的符號,并不是mem_ckeck自身的符號,所以我們探測的符號修改libc庫中malloc符號,系統中可能存在多個c庫,我們需要找到mem_ckeck程序使用的C庫,通過ldd命令查看:
mem_check可執行文件使用的C庫為:/lib/x86_64-linux-gnu/libc.so.6,我們將可以執行文件替換為/lib/x86_64-linux-gnu/libc.so.6。再次執行,會出現大量內容,顯然是其他進程調用了malloc引起的,而我們的mem_ckeck還沒有運行,顯然還沒有探測我們的可執行程序。
我們需要進行過濾,增加filter只保留我們關心的應用程序的調用探測。bpftrace提供了系統變量comm表示可執行文件名 (進程名),只需要在上述指令中增加 filter,只處理comm=="mem_check"的malloc調用事件。左邊終端執行探測,右邊終端執行可執行文件。每調用一次malloc函數,就能探測到一次:
使用bpftrace腳本進一步探測
將上面的單行命令變為bpftrace腳本--bpf_test.bt
printf("start proben");
}
uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc /comm == "mem_check"/{
printf("malloc calln");
}
END {
printf("end proben");
}
探測mem_check中malloc的內存空間大小。
malloc的原型:
bpftrace的uprobe和kprobe可以通過內置變量arg0、arg1 ··· ··· 訪問函數參數,對bpf_test.bt修改就可以打印malloc申請內存的大小:
printf("start proben");
}
uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc /comm == "mem_check"/{
printf("malloc size: %dn", arg0);
}
END {
printf("end proben");
}
如下圖可以看到mem_check中申請內存的情況,最后一個malloc size 1024是mem_check自動創建輸出緩沖區申請的內存,不用理會。
探測mem_check中malloc的返回值
malloc的返回值是地址,需借助uretprobe進行探測,函數返回值可通過內置變量retval訪問。uretprobe的filter與malloc參數探測時類似,腳本修改為:
printf("start proben");
}
uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc /comm == "mem_check"/{
printf("malloc size: %dn", arg0);
}
uretprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc /comm == "mem_check"/{
printf("addr = %pn", retval);
}
END {
printf("end proben");
}
運行結果:
探測mem_check中free
我們已經探測到mem_check的malloc的內存大小,內存的地址,我們通過探測free,然后匹配malloc和free的情況就可以查找內存的泄漏點。腳本修改為:
printf("start proben");
}
uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc /comm == "mem_check"/{
printf("malloc size: %dn", arg0);
}
uretprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc /comm == "mem_check"/{
printf("addr = %pn", retval);
}
uprobe:/lib/x86_64-linux-gnu/libc.so.6:free /comm == "mem_check"/{
printf("free addr = %pn", arg0);
}
END {
printf("end proben");
}
運行結果:
探測內存泄露
上面我們已經探測到了mem_check中的malloc,free情況。我們可以通過malloc和free的地址集合差,就可以得到內存泄露的地址位置。
bpftrace底層使用的是eBPF的map作為存儲結構,可以簡單的看作K-V存儲,我們可以利用map來統計地址集合差,步驟如下:
- 定義一個map變量@mem:保存malloc返回的內存地址。
- 當探測到free調用時,將@mem對應地址刪除。
- 最后@mem剩下的就是內存泄露的地址。
內存泄露檢測腳本如下:
printf("start proben");
}
uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc /comm == "mem_check"/{
printf("malloc size: %dn", arg0);
@size = arg0;
}
uretprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc /comm == "mem_check"/{
printf("addr = %pn", retval);
@mem[retval] = @size;
}
uprobe:/lib/x86_64-linux-gnu/libc.so.6:free /comm == "mem_check"/{
printf("free addr = %pn", arg0);
delete(@mem[arg0]);
}
END {
printf("end proben");
}
運行結果:
如上圖,紅色框中就是沒有釋放的內存和內存大小。
總結
通過編寫一些簡單的bpftrace腳本,我們就可以監視應用程序的內存分配和釋放事件,捕獲內存泄漏的跡象。這種直接的實時監控方式,使得開發者能夠在問題出現時即刻獲得反饋,從而更加迅速地解決潛在的內存問題,提升軟件的穩定性和性能。
-
內存
+關注
關注
8文章
3002瀏覽量
73887 -
代碼
+關注
關注
30文章
4751瀏覽量
68357 -
Valgrind
+關注
關注
0文章
9瀏覽量
6797
發布評論請先 登錄
相關推薦
評論