[openssl] 內存泄露及越界分析方法整理


前言

[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

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM