1
头图

内存泄漏简介

非法内存访问错误很快就会暴露出来,但不同的是,内存泄漏错误有时即使长时间运行也不会暴露出来,这就导致内存泄漏错误是不易被发现的。内存泄漏可能并不严重,甚至无法通过正常方式检测到。在现代操作系统中,应用程序使用的内存在应用程序终止时由操作系统统一释放,这意味着只运行很短时间的程序中的内存泄漏可能不会被注意到并且很少是严重的。

然而内存泄漏带来的危害是不可忽视的,内存泄漏会导致系统可用内存逐步减少从而间接降低计算机的性能,极端情况下,当系统的可用内存被耗尽,此时系统全部或部分停止工作,系统无法启动新的应用程序,系统可能由于抖动而大大减慢,这种情况通常只能够通过重启系统才能够让系统恢复。

lsan 简介

lsan 是一个运行时内存泄漏检测器,它可以与 asan 结合使用以同时获得检测内存访问错误和内存泄漏的能力,它也可以单独使用。lsan 是在进程结束时才开始泄漏检测,因此它几乎不会降低程序的性能。

开启 lsan

lsan 支持两种运行模式,这两种运行模式下,lsan 的开启方式不同。

和 asan 一起运行 
通过运行时标识 detect_leaks 来开启,asan 支持如下两种方式来传递运行时标志:

  1. 环境变量 ASAN_OPTIONS:

ASAN_OPTIONS=detect_leaks=1

  1. 函数 __asan_default_options
const char*__asan_default_options() { return "detect_leaks=1"; }

完整例子参见:

https://github.com/dengking/s...

lsan 在 x86_64 Linux 的 asan 版本中默认启用。

Stand-alone mode 
对性能要求高的场景可能无法接受由于 asan 而引入的性能损耗,此时可以只开启 lsan。

对于 cmake,方法如下:

set (CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -fsanitize=leak")
set (CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=leak")
set (CMAKE_LINKER_FLAGS_DEBUG "${CMAKE_LINKER_FLAGS_DEBUG} -fsanitize=leak")

Run-time flags

和 asan 一样,lsan 也支持通过 run-time flag 来对它的行为进行调整。

设置 flag 的两种方式

lsan 支持如下两种方式来传递 run-time flags,工程师可以根据实际情况选取合适的方式。

  • LSAN_OPTIONS

LSAN_OPTIONS 是环境变量,在不同的 OS 中,设置的方式不同。

  • __lsan_default_options 函数

使用该函数来将 run-time flags 嵌入代码中,例子: 

https://github.com/dengking/s...

查看完整的 run-time flag

和 asan 一样,lsan 也支持通过 help=1 来查看完整的 run-time flag,完整例子参见:

https://github.com/dengking/s...

Suppression file

可以通过传入 suppression file 来指示 lsan 忽略某些泄漏。suppression file 的每行必须包含一个 suppression rule,suppression rule 的格式为:

leak:<pattern>

在发现泄漏后,lsan 会将该泄漏的 stack trace 进行符号化,然后与进行子字符串匹配,如果函数名、源文件名或二进制文件名匹配,那么这个泄漏报告将被禁止。下面是一个例子:

$ cat suppr.txt 
# This is a known leak.
leak:FooBar
$ cat lsan-suppressed.cc 
#include <stdlib.h>

void FooBar() {
  malloc(7);
}

void Baz() {
  malloc(5);
}

int main() {
  FooBar();
  Baz();
  return 0;
}
$ clang++ lsan-suppressed.cc -fsanitize=address
$ ASAN_OPTIONS=detect_leaks=1 LSAN_OPTIONS=suppressions=suppr.txt ./a.out

=================================================================
==26475==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 5 byte(s) in 1 object(s) allocated from:
    #0 0x44f2de in malloc /usr/home/hacker/llvm/projects/compiler-rt/lib/asan/asan_malloc_linux.cc:74
    #1 0x464e86 in Baz() (/usr/home/hacker/a.out+0x464e86)
    #2 0x464fb4 in main (/usr/home/hacker/a.out+0x464fb4)
    #3 0x7f7e760b476c in __libc_start_main /build/buildd/eglibc-2.15/csu/libc-start.c:226

-----------------------------------------------------
Suppressions used:[design document](AddressSanitizerLeakSanitizerDesignDocument)
  count      bytes template
      1          7 FooBar
-----------------------------------------------------

SUMMARY: AddressSanitizer: 5 byte(s) leaked in 1 allocation(s).

 上述例子源自: 

https://github.com/google/san...

<pattern> 支持正则表达式语法,比如特殊符号 ^ 和 $ 匹配字符串的开头和结尾。

lsan 的原理

官方文档:

https://github.com/google/san...

术语说明

为便于论述遍历,定义如下术语:

image.png

原理概述

lsan 在进程 exit 的前一刻被触发,它首先暂停进程("StopTheWorld"),然后扫描进程的 memory,lsan将进程的内存分为两大类:

heap region

live memory

lsan 扫描进程的 live memory 以发现 live pointer,然后校验每个 heap block 是否有 live pointer 指向它,如果没有,那么 lsan 就判定它被泄漏了。

详细论述

与 asan 不同,lsan 并不需要编译器对源代码进行转换以插入检查代码,lsan 由如下两部分组成:

leak checking module

run-time environment,它包括:

  • memory allocator
  • thread registry
  • interceptors(拦截器)for memory allocation / thread management functions

其中,run-time environment 是大多数 sanitizer 工具共享的通用组件,由于 lsan 和 asan 的实现并不冲突,因此 lsan 能够在 asan 之上运行。

在目标进程结束之前,leak checking module 都处于非活动状态,它在进程 exit 的前一刻被触发,它会停止目标进程的执行,然后检查目标进程的内存泄漏。在具体实现上,lsan 会以进程的方式运行,它使用 ptrace 附加到目标进程。

关于 ptrace,参见:

https://stackoverflow.com/que...

https://en.wikipedia.org/wiki...

live memory

live memory 必须至少包括以下内容:

global variables

stacks of running threads

general-purpose registers of running threads

ELF thread-local storage and POSIX thread-specific data

lsan 首先在 live memory 中查找 live pointer:它扫描上述内存以查找指向 heap block 指针的字节模式,lsan 将内部指针与指向 heap block 开头的指针视为相同。对于有 live pointer 指向的 heap block,lsan 认为是可访问的,它们的内容也被视为“live memory”,因此 lsan 也需要扫描它们的内容以发现 live pointer,显然这个过程非常类似于求解闭包。通过这种方式,lsan 能够发现了从“live memory”中可访问的所有 heap block。lsan 会在 heap block 的元数据中设置一个标志来标记这些可访问的 heap block。然后 lsan 遍历所有现有的 heap block 并将无法访问的 heap block 报告为泄漏,为了便于排查问题,在报告泄漏的同时 lsan 会将这些泄漏的 heap block 的动态分配函数执行过程的 stack trace 一并输出。

lsan 的另一个有用的功能是能够区分直接泄漏的 heap block(无法从任何地方访问)和间接泄漏的 heap block(可从其他泄漏的 heap block 访问)。这是通过对标记为泄漏的 heap block 使用上述相同的算法来实现的。

如何获得 live memory

live memory 的获取依赖于 OS 提供 system call,关于此在

 (https://github.com/google/san...)中进行了详细的介绍,下面是简单的梳理:

image.png

False negative

lsan 算法是存在 false negative 的,在笔者的 GitHub 仓库

https://github.com/dengking/s...)的 false-negative

https://github.com/dengking/s...)中整理了 lsan 无法检测出泄漏、少检测出泄漏的例子,下面结合一些典型的例子进行说明。

有 live pointer 指向的 heap region

按照 lsan 的原理,它会将没有 live pointer 指向的 heap region 标记为泄漏;对于有 live pointer 指向的 heap region,如果在 process 退出的时候,它依然没有被释放,lsan 并不会将它标记为泄漏,这种情况是否算是泄漏其实很难界定,因为 OS 会在进程退出的时候,统一回收进程占用的资源,所以即使工程师没有释放,大多数情况下是没有问题的,但是如果程序的正确性依赖于某个 heap object 的 destructor 的执行,那么这种情况下应用程序可能是错误的。另外从严格的工程实践来说,工程师应该保证程序的完全正确,应该对资源管理完全负责,所以本文会将这种情况定义为泄漏,将这种情况视为 lsan 的 False negative。实际上,这种情况,目前基本上是没有工具能够检测出来的。

下面的例子展示了这种情况:

image.png

最最典型的是:

https://github.com/dengking/s...

完整代码如下:

#include <stdlib.h>

void *global;

int main()
{
    global = malloc(7);
    return 0;
}

global 变量是一个作用域为全局的指针,显然它所指向的 heap region 没有被释放,上述程序 lsan 并不会报泄漏,但是实际上 global 所指向的 heap region 被泄漏了,仅就这个程序而言,这个泄漏是不会带来错误的。

std::vector 的一些例子

下面的这个例子是源自:

https://github.com/google/san...

#include <vector>
std::vector<int *> *global;
int main(int argc, char **argv)
{
    global = new std::vector<int *>;
    global->push_back(new int[10]);
    global->push_back(new int[20]);
    global->push_back(new int[30]);
    global->push_back(new int[40]);
    global->pop_back(); // The last element leaks now.
    return 0;
}

完整代码参见:https://github.com/dengking/s...

严格来说,上述程序实际存在 5 处泄漏(5处 new),但是按照 lsan 的算法,仅仅注释标注的语句引入了一处泄漏。从https://github.com/google/san...

中的内容可知,这种泄漏的检测是有赖于对 std::vector 进行特殊实现的,否则由于 std::vector 使用 lazy-deallocate 技术‍(https://github.com/dengking/s...)那么即使执行了 global->pop_back(),最后一个元素其实并没有从 vector 的 heap 中删除,这样 lsan 会认为它依然是 live pointer。

除了上述例子,std::vector  的 false negative 例子还包括:

image.png

总结

从目前的验证来看,lsan 存在着一些 false negative。

编译器支持情况

‍下面是 clang 文档(https://clang.llvm.org/docs/L...) 中给出的 LLVM  clang 的支持情况,本文不在赘述。需要强调的是: Apple clang 不支持 lsan,在 macOS 上 一般使用 instrument 来检测泄漏。除此之外,MSVC 也不支持 lsan,下面会进行详细说明。

Windows 不支持 lsan

Windows 尚不支持 lsan,具体原因如下: 

通过前面的介绍可知,lsan 需要能够在进程退出或其他时间点停止进程以扫描 live pointer,posix 实现使用 ptrace, Windows 暂不支持 ptrace,所以无法实现 lsan。

更多内容,参见:https://github.com/google/san...

lsan 的 example code

相较于 asan,lsan 的功能比较单一,lsan 的功能集中在对内存泄漏的检测。在

https://github.com/dengking/s...

https://github.com/dengking/s...

https://github.com/dengking/s...

中整理了多个例子,这些例子涵盖了 C、C++,涵盖了 local object、global object,涵盖了 OOP,涵盖了一些典型的 STL container 等领域,旨在帮助读者快速上手。


网易数智
619 声望140 粉丝

欢迎关注网易云信 GitHub: