前言
[classic_tong: https://www.cnblogs.com/hugetong/p/14386531.html]
圍繞着 [openssl] openssl asynch_mode 使用libasan時的OOM問題
以及 https://github.com/intel/QAT_Engine/issues/178 的處理過程,先后嘗試了幾個內存問題檢測的工具和方法,
現將其總結討論在本文中。
概述
內存問題檢測的工具和方法有很多,大概就分兩類,一類是編譯期介入的,一類是運行時介入的。本文僅有限討論如下幾種:
gperftools,valgrind,libasan,DIY,systemtap,ptrace
比較
借libasan的一個表格,進行一下比較:
原始表格見: https://github.com/google/sanitizers/wiki/AddressSanitizerComparisonOfMemoryTools
gperftools
是一組google的工具集。我們現在討論的是其中的一個工具,叫做heap checker。 它依賴tcmalloc,在運行時生效。
有兩個辦法使heap checker生效。第一,在編譯連接時加入-ltcmalloc。第二,在運行是使用LD_PRELOAD。顯然在不使用tcmalloc的時候,后者更方便。
heap checker的原理是hook掉標准的malloc和free。然后在里邊進行內存的檢測。
簡單的使用步驟:
1. 安裝
yum install gperftools-libs-2.6.1-1.el7.x86_64.rpm
2. 運行
export LD_PRELOAD=/usr/lib64/libtcmalloc.so.4.4.5 export HEAPCHECK=normal HEAPPROFILE="/root/debug/src/test/heapprof.log" ./async
運行后,內存情況,會被周期打印到這個日志文件里.
文檔中包含更詳細的用法,以及更多配置。
主頁:https://github.com/gperftools/gperftools
文檔頁:https://gperftools.github.io/gperftools/heap_checker.html
valgrind
從我的理解,valgrind就是個模擬器。被調試程序無感的跑在它的上面。程序的所有的讀寫與指令都寫入到模擬器上去。基於這種運行模式
它擁有以下幾個特點:
1. 運行速度慢。程序運行速度會下降40倍。所以很多以前出現的問題,可能由於速度變慢而不出現了。
2. 功能強大。因為作為模擬器跑着程序下面。
3. 不准。會有假陽性。因為它把程序看做黑盒,通過對我行為反推。所以會假陽性。( false positive )
使用
valgrind的用法算是最簡單的了。把valgrind及其參數放置在本測試程序的前面執行。如下:
[root@T9 ~]# valgrind --log-file=/tmp/valgrind.log --trace-children=yes --read-inline-info=yes --read-var-info=yes -v ./a.out Segmentation fault [root@T9 ~]# ll /tmp/ -rw-r--r-- 1 root root 5258 Jan 25 16:07 valgrind.log -rw------- 1 root root 8470528 Jan 25 16:06 valgrind.log.core.3650 -rw------- 1 root root 8470528 Jan 25 16:07 valgrind.log.core.3658
參數:
1 當使用--log-file參數的時候, valgrind的輸出結果將寫着這個地方。這對daemon程序的調試十分有用。另外,如果發生了coredump的話,core文件會寫着與log文件同一個位置.
不過在我的實踐中,這個coredump並不能正常的使用gdb打開,可能需要特殊的方式,目前還不會用。
2 valgrind的功能不僅僅局限於內存檢測。它實際上是一組工具集。用--tool=memcheck指定,也可以不指定,因為memcheck是默認工具。
文檔:https://valgrind.org/docs/manual/quick-start.html https://valgrind.org/docs/manual/manual.html
這文檔,清晰又好讀。建議讀一下。
libasan
LLVM是一個編譯器后端,一般與clang作為前端配合。另外,gcc也可以作為llvm的前端,或者說gcc使用llvm作為其后端。見:https://dragonegg.llvm.org/
什么前端后端?見:https://blog.csdn.net/xhhjin/article/details/81164076
現在進入正題,libasan是llvm的一個組成部分,由google開發。gcc里邊有對用的clone。但是只有基本的clone,很多高級功能還不支持。
gcc里的用法:(以下內容僅在gcc4.8.5測試過)
用llvm編譯的話,不用單裝任何東西,直接用就可以。代碼在這個地方:https://github.com/google/sanitizers
gcc sanitize的代碼在gcc里邊:https://github.com/google/sanitizers。CentOS里作為一個單獨的庫進行發布,需要單獨安裝,就是libasan,:
[root@T9 ~]# rpm -qa |grep libasan libasan-static-4.8.5-44.el7.x86_64 libasan-4.8.5-44.el7.x86_64
靜態方法
CFLAGS+=-fsanitize=address -fno-omit-frame-pointer -I /root/debug/include/ -static-libasan LDFLAGS+= -fsanitize=address -fno-omit-frame-pointer -lssl -lcrypto -static-libasan
動態方法
CFLAGS+=-fsanitize=address -fno-omit-frame-pointer LDFLAGS+=-fsanitize=address -fno-omit-frame-pointer LDFLAGS+= -lasan
注意:
1. 編譯和鏈接選項都要有這兩“-fsanitize=address -fno-omit-frame-pointer”, 不然會在運行時報錯。至少在我使用nginx+openssl時,是這樣的。
2. 如果openssl是帶着libasan選項編譯的。那么因為nginx link了openssl的so,所以也要使用libasan的選項編譯。不然,一樣會運行時報錯。
參數和選項
可以配各種參數,這些參數可以在libasan的文檔里找到,但是gcc實現里好不好用,還得試試才知道。我用過的幾個如下所示
配置日志的方法
1. 環境變量
export ASAN_OPTIONS="log_path=asan.log" ./mytests
注: 在main函數開頭,使用setenv設置該環境變量,並不好用.
2. 代碼寫死
#include <sanitizer/asan_interface.h> __sanitizer_set_report_path("/tmp/asan.log")
3 默認情況全部寫入標准錯誤
參考: https://stackoverflow.com/questions/39686628/how-to-set-asan-ubsan-reporting-output
其他選項
多個用冒號分隔: ASAN_OPTIONS=verbosity=1:malloc_context_size=20 ./a.out
https://github.com/google/sanitizers/wiki/AddressSanitizerFlags
https://github.com/google/sanitizers/wiki/SanitizerCommonFlags
DIY
就是說,自己寫程序檢測內存問題。主要兩方面。
1. 堆內存的泄露
自己實現一個hash表,new的時候插入,free的時候刪除。可以同時帶上調用棧信息。
最后,程序結束的時候,hash表里剩下的就是內存泄露的。下面是一個我的例子。
https://github.com/tony-caotong/knickknack/blob/master/examples/mem_dbg/mem_dbg.c
2. 堆內存的越界
堆內存的越界檢測原理是,在內存的開始和結尾各自多申請一小段,比如前后各4字節。在new時寫入特定內容,比如1234
在free時檢測是否依然是1234. 如果不是,說頭越界或者尾越界了。
更多詳細的方法,可以參考libasan的實現,https://github.com/google/sanitizers/wiki/AddressSanitizerAlgorithm
該場景適用於:1, 內存池的泄露檢測。2,非標准api申請的內存,比如intel qatdriver的連續內存管理模塊就是這樣,直接使用內核module分配。
systemtap
這個也屬於DIY的范疇,只不過是通過systemtap機制實現。
有關systemtap的簡單背景內容,請閱讀一下,再回來:[optimize]使用systemtap調試用戶態程序
如該文所闡述的方法,我們可以hook一個腳本到被調試程序中,在該腳本中實現上一小結的hash表邏輯,完成同樣功能。
與前面方法的對比,有兩個異同:
1. 使用systemtap,可以在運行時介入,原程序無感,不需要重新編譯,不需要中斷運行。隨時修改隨時上。
2. systemtap的語法腳本有學習門檻。
所以,更推薦用systemtap,能用就用。
另外,提到systemtap就不得不提Dtrace,Dtrace更高級。
DTrace 跟 systemtap功能是一樣的. linux內核4.9之后支持DTrace, linux內核3.5之后支持systemtap
ptrace
有些場景,比如某個變量指向的內存,總是被寫壞。但是用以上方法又沒有發現問題。可以用gdb watchpoint。
但是並不是所有環境都可以方便的使用gdb。我們還可以使用gdb的底層工具ptrace,自行編碼實現調試需求。
watchpoint是什么(我也沒搞懂):https://sourceware.org/gdb/current/onlinedocs/gdb/Breakpoints.html#Breakpoints
ptrace的文檔:https://man7.org/linux/man-pages/man2/ptrace.2.html
更多
還有更高級的,eBPF:http://www.brendangregg.com/blog/2019-01-01/learn-ebpf-tracing.html