zoukankan      html  css  js  c++  java
  • [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

     
  • 相关阅读:
    FMDB的简单使用
    SQLite3的基本使用
    KVC与KVO的实现原理
    数据存储与IO(二)
    数据存储与IO(一)
    cocoapods卸载与安装的各种坑
    Core Data的一些常见用法
    UITextField限制中英文字数和光标定位以及第三方输入限制问题
    prompt-tuning paper reading
    ACL2021 事件抽取相关论文阅读
  • 原文地址:https://www.cnblogs.com/hugetong/p/14386531.html
Copyright © 2011-2022 走看看