基於 ebpf 的性能工具 - bpftrace 實戰 -內存泄漏-

在實際的軟件開發過程中,內存問題常常是耗費大量時間進行分析的挑戰之一。爲了更有效地定位和解決與內存相關的難題,一系列輔助工具應運而生,其中備受讚譽的 Valgrind 工具便是其中之一。事實上,筆者本人曾利用 Valgrind 工具成功地發現並解決了一個隱藏在軟件中的 bug,這充分體現了工具在開發過程中的重要性。

然而,同樣強大的 bpftrace 工具同樣具備簡潔而直觀的特點,能夠協助我們高效地追蹤內存泄漏問題。在這方面,bpftrace 提供了一種更加精細的、實時的分析方式,幫助開發人員準確地定位代碼中可能存在的內存泄漏情況。

構建樣例


我們編寫一個程序 --mem_check.c,代碼中包含正確的申請內存和釋放內存的邏輯,同時包含存在內存泄露的代碼代碼。。

#include <stdio.h>
#include <stdlib.h>

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 文件,生成可執行文件:

gcc mem_check.c -o mem_check

探測 mem_ckeck 可執行文件


bpftrace 可以對內核態進行探測也可以對用戶態進行探測,其中探針如下:

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 還沒有運行,顯然還沒有探測我們的可執行程序。

bpftrace -e 'uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc {printf("malloc call\n")}'

我們需要進行過濾,增加 filter 只保留我們關心的應用程序的調用探測。bpftrace 提供了系統變量 comm 表示可執行文件名 (進程名),只需要在上述指令中增加 filter,只處理 comm=="mem_check" 的 malloc 調用事件。左邊終端執行探測,右邊終端執行可執行文件。每調用一次 malloc 函數,就能探測到一次:

使用 bpftrace 腳本進一步探測


將上面的單行命令變爲 bpftrace 腳本 --bpf_test.bt

BEGIN {
    printf("start probe\n");
}

uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc /comm == "mem_check"/{
  printf("malloc call\n");
}

END {
    printf("end probe\n");
}

探測 mem_check 中 malloc 的內存空間大小。

malloc 的原型:

void *malloc(size_t size);

bpftrace 的 uprobe 和 kprobe 可以通過內置變量 arg0、arg1 ··· ··· 訪問函數參數,對 bpf_test.bt 修改就可以打印 malloc 申請內存的大小:

BEGIN {
    printf("start probe\n");
}

uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc /comm == "mem_check"/{
  printf("malloc size: %d\n", arg0);
}

END {
    printf("end probe\n");
}

如下圖可以看到 mem_check 中申請內存的情況,最後一個 malloc size 1024 是 mem_check 自動創建輸出緩衝區申請的內存,不用理會。

探測 mem_check 中 malloc 的返回值

malloc 的返回值是地址,需藉助 uretprobe 進行探測,函數返回值可通過內置變量 retval 訪問。uretprobe 的 filter 與 malloc 參數探測時類似,腳本修改爲:

BEGIN {
    printf("start probe\n");
}

uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc /comm == "mem_check"/{
    printf("malloc size: %d\n", arg0);
}

uretprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc /comm == "mem_check"/{
  printf("addr = %p\n", retval);
}

END {
    printf("end probe\n");
}

運行結果:

探測 mem_check 中 free

我們已經探測到 mem_check 的 malloc 的內存大小,內存的地址,我們通過探測 free,然後匹配 malloc 和 free 的情況就可以查找內存的泄漏點。腳本修改爲:

BEGIN {
    printf("start probe\n");
}

uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc /comm == "mem_check"/{
    printf("malloc size: %d\n", arg0);
}

uretprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc /comm == "mem_check"/{
  printf("addr = %p\n", retval);
}

uprobe:/lib/x86_64-linux-gnu/libc.so.6:free /comm == "mem_check"/{
  printf("free addr = %p\n", arg0);
}

END {
    printf("end probe\n");
}

運行結果:

探測內存泄露


上面我們已經探測到了 mem_check 中的 malloc,free 情況。我們可以通過 malloc 和 free 的地址集合差,就可以得到內存泄露的地址位置。

bpftrace 底層使用的是 eBPF 的 map 作爲存儲結構,可以簡單的看作 K-V 存儲,我們可以利用 map 來統計地址集合差,步驟如下:

  1. 定義一個 map 變量 @mem:保存 malloc 返回的內存地址。

  2. 當探測到 free 調用時,將 @mem 對應地址刪除。

  3. 最後 @mem 剩下的就是內存泄露的地址。

內存泄露檢測腳本如下:

BEGIN {
    printf("start probe\n");
}

uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc /comm == "mem_check"/{
    printf("malloc size: %d\n", arg0);
    @size = arg0;
}

uretprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc /comm == "mem_check"/{
    printf("addr = %p\n", retval);
    @mem[retval] = @size;

}

uprobe:/lib/x86_64-linux-gnu/libc.so.6:free /comm == "mem_check"/{
    printf("free addr = %p\n", arg0);
    delete(@mem[arg0]);
}

END {
    printf("end probe\n");
}

運行結果:

如上圖,紅色框中就是沒有釋放的內存和內存大小。

總結


通過編寫一些簡單的 bpftrace 腳本,我們就可以監視應用程序的內存分配和釋放事件,捕獲內存泄漏的跡象。這種直接的實時監控方式,使得開發者能夠在問題出現時即刻獲得反饋,從而更加迅速地解決潛在的內存問題,提升軟件的穩定性和性能。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/s0hr-kc6bUnZiM-CINUe8w