[openssl] 内存泄露及越界分析方法整理 前言 概述 比较 gperftools valgrind libasan DIY systemtap ptrace 更多

[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的一个表格,进行一下比较:

[openssl] 内存泄露及越界分析方法整理
前言
概述
比较
gperftools
valgrind
libasan
DIY
systemtap
ptrace
更多

原始表格见: 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://*.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