乐趣区

关于系统:Sanitizers-系列之-leak-sanitizer-介绍

内存透露简介

非法内存拜访谬误很快就会裸露进去,但不同的是,内存透露谬误有时即便长时间运行也不会裸露进去,这就导致内存透露谬误是不易被发现的。内存透露可能并不重大,甚至无奈通过失常形式检测到。在古代操作系统中,应用程序应用的内存在应用程序终止时由操作系统对立开释,这意味着只运行很短时间的程序中的内存透露可能不会被留神到并且很少是重大的。

然而内存透露带来的危害是不可漠视的,内存透露会导致系统可用内存逐渐缩小从而间接升高计算机的性能,极其状况下,当零碎的可用内存被耗尽,此时零碎全副或局部进行工作,零碎无奈启动新的应用程序,零碎可能因为抖动而大大减慢,这种状况通常只可能通过重启零碎才可能让零碎复原。

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…

术语阐明

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

原理概述

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…)中进行了具体的介绍,上面是简略的梳理:

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。实际上,这种状况,目前基本上是没有工具可能检测进去的。

上面的例子展现了这种状况:

最最典型的是:

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 例子还包含:

总结

从目前的验证来看,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 等畛域,旨在帮忙读者疾速上手。

退出移动版