作为 C/ C++ 工程师,在开发过程中会遇到各类问题,最常见便是内存使用问题,比如,越界泄漏。过去常用的工具是 Valgrind,但使用 Valgrind 最大问题是它会极大地降低程序运行的速度,初步估计会降低 10 倍运行速度。而 Google 开发的 AddressSanitizer 这个工具很好地解决了 Valgrind 带来性能损失问题,它非常快,只拖慢程序 2 倍速度。

AddressSanitizer 概述

AddressSanitizer 是一个基于编译器的测试工具,可在运行时检测 C/C++ 代码中的多种内存错误。严格上来说,AddressSanitizer 是一个编译器插件,它分为两个模块,一个是编译器的 instrumentation 模块,一个是用来替换 malloc/free 的动态库。

Instrumentation 主要是针对在 llvm 编译器级别对访问内存的操作(store,load,alloc等),将它们进行处理。动态库主要提供一些运行时的复杂的功能(比如 poison/unpoison shadow memory)以及将 malloc/free 等系统调用函数 hook 住。

AddressSanitizer 基本使用

根据 AddressSanitizer Wiki 可以检测下面这些内存错误

  • Use after free:访问堆上已经被释放的内存
  • Heap buffer overflow:堆上缓冲区访问溢出
  • Stack buffer overflow:栈上缓冲区访问溢出
  • Global buffer overflow:全局缓冲区访问溢出
  • Use after return:访问栈上已被释放的内存
  • Use after scope:栈对象使用超过定义范围
  • Initialization order bugs:初始化命令错误
  • Memory leaks:内存泄漏

这里我只简单地介绍下基本的使用,详细的使用文档可以看官方的编译器使用文档,比如 Clang 的文档:https://clang.llvm.org/docs/AddressSanitizer.html

Use after free 实践例子

下面这段代码是一个很简单的 Use after free 的例子:

//use_after_free.cpp
#include <iostream>
int main(int argc, char **argv) {
int *array = new int[100];
delete [] array;
std::cout << array[0] << std::endl;
return 1;
}

编译代码,并且运行,这里可以看到只需要在编译的时候带上 -fsanitize=address  选项就可以了。

clang++  -O -g -fsanitize=address ./use_after_free.cpp
./a.out

最终我们会看到如下的输出:

==10960==ERROR: AddressSanitizer: heap-use-after-free on address 0x614000000040 at pc 0x00010d471df0 bp 0x7ffee278e6b0 sp 0x7ffee278e6a8
READ of size 4 at 0x614000000040 thread T0
#0 0x10d471def in main use_after_free.cpp:6
#1 0x7fff732c17fc in start (libdyld.dylib:x86_64+0x1a7fc) 0x614000000040 is located 0 bytes inside of 400-byte region [0x614000000040,0x6140000001d0)
freed by thread T0 here:
#0 0x10d4ccced in wrap__ZdaPv (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x51ced)
#1 0x10d471ca1 in main use_after_free.cpp:5
#2 0x7fff732c17fc in start (libdyld.dylib:x86_64+0x1a7fc) previously allocated by thread T0 here:
#0 0x10d4cc8dd in wrap__Znam (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x518dd)
#1 0x10d471c96 in main use_after_free.cpp:4
#2 0x7fff732c17fc in start (libdyld.dylib:x86_64+0x1a7fc) SUMMARY: AddressSanitizer: heap-use-after-free use_after_free.cpp:6 in main

可以看到一目了然,非常清楚的告诉了我们在哪一行内存被释放,而又在哪一行内存再次被使用。

还有一个是内存泄漏,比如下面的代码,显然 p 所指的内存没有被释放。

void *p;

int main() {
p = malloc(7);
p = 0; // The memory is leaked here.
return 0;
}

编译然后运行

clang -fsanitize=address -g  ./leak.c
./a.out

可以看到如下的结果

=================================================================
==17756==ERROR: LeakSanitizer: detected memory leaks Direct leak of 7 byte(s) in 1 object(s) allocated from:
#0 0x4ffc80 in malloc (/home/simon.liu/workspace/a.out+0x4ffc80)
#1 0x534ab8 in main /home/simon.liu/workspace/./leak.c:4:8
#2 0x7f127c42af42 in __libc_start_main (/usr/lib64/libc.so.6+0x23f42) SUMMARY: AddressSanitizer: 7 byte(s) leaked in 1 allocation(s).

不过这里要注意内存泄漏的检测只会在程序最后退出之前进行检测,也就是说如果你在运行时如果不断地分配内存,然后在退出的时候对内存进行释放,AddressSanitizer 将不会检测到内存泄漏,这种时候可能你就需要另外的工具了 JeMalloc / TCMalloc。

AddressSanitizer 基本原理

这里简单介绍一下 AddressSanitizer 的实现,更详细的算法实现可以看《AddressSanitizer: a fast address sanity checker》:https://www.usenix.org/system/files/conference/atc12/atc12-final39.pdf

AddressSanitizer 会替换你的所有 malloc 以及 free,然后已经被分配(malloc)的内存区域的前后会被标记为 poisoned (主要是为了处理 overflow 这种情况),而释放(free)的内存会被标记为 poisoned(主要是为了处理 Use after free)。你的代码中的每一次的内存存取都会被编译器做类似下面的翻译.

before:

*address = ...;  // or: ... = *address;

after:

shadow_address = MemToShadow(address);
if (ShadowIsPoisoned(shadow_address)) {
ReportError(address, kAccessSize, kIsWrite);
}
*address = ...; // or: ... = *address;

这里可以看到首先会对内存地址有一个翻译(MemToShadow)的过程,然后再来判断当所访问的内存区域是否为 poisoned,如果是则直接报错并退出。

这里之所以会有这个翻译是因为 AddressSanitizer 将虚拟内存分为了两部分:

  1. Main application memory(Mem)也就是被当前程序自身使用的内存
  2. Shadow memory 简单来说就是保存了主存元信息的一块内存,比如主存的那些区域被 posioned 都是在 Shadow memory 中保存的

AddressSanitizer 和其他内存检测工具对比

下图是 AddressSanitizer 与其他的一些内存检测工具的对比:

AddressSanitizer Valgrind/Memcheck Dr. Memory Mudflap Guard Page gperftools
technology CTI DBI DBI CTI Library Library
ARCH x86, ARM, PPC x86, ARM, PPC, MIPS, S390X, TILEGX x86 all(?) all(?) all(?)
OS Linux, OS X, Windows, FreeBSD, Android, iOS Simulator Linux, OS X, Solaris, Android Windows, Linux Linux, Mac(?) All (1) Linux, Windows
Slowdown 2x 20x 10x 2x-40x ? ?
Detects:
Heap OOB yes yes yes yes some some
Stack OOB yes no no some no no
Global OOB yes no no ? no no
UAF yes yes yes yes yes yes
UAR yes (see AddressSanitizerUseAfterReturn) no no no no no
UMR no (see MemorySanitizer) yes yes ? no no
Leaks yes (see LeakSanitizer) yes yes ? no yes

参数说明:

  • DBI: dynamic binary instrumentation(动态二进制插桩)
  • CTI: compile-time instrumentation (编译时插桩)
  • UMR: uninitialized memory reads (读取未初始化的内存)
  • UAF: use-after-free (aka dangling pointer) (使用释放后的内存)
  • UAR: use-after-return (使用返回后的值)
  • OOB: out-of-bounds (溢出)
  • x86: includes 32- and 64-bit.

可以看到相比于 Valgrind,AddressSanitizer 只会拖慢程序 2 倍运行速度。当前 AddressSanitizer 支持 GCC 以及 Clang,其中 GCC 是从 4.8 开始支持,而 Clang 的话是从 3.1 开始支持。

AddressSanitizer 的使用注意事项

  1. AddressSanitizer 在发现内存访问违规时,应用程序并不会自动崩溃。这是由于在使用模糊测试工具时,它们通常都是通过检查返回码来检测这种错误。当然,我们也可以在模糊测试进行之前通过将环境变量 ASAN_OPTIONS 修改成如下形式来迫使软件崩溃:
export ASAN_OPTIONS='abort_on_error=1'/
  1. AddressSanitizer 需要相当大的虚拟内存(大约 20 TB),不用担心,这个只是虚拟内存,你仍可以使用你的应用程序。但像 american fuzzy lop 这样的模糊测试工具就会对模糊化的软件使用内存进行限制,不过你仍可以通过禁用内存限制来解决该问题。唯一需要注意的就是,这会带来一些风险:测试样本可能会导致应用程序分配大量的内存进而导致系统不稳定或者其他应用程序崩溃。因此在进行一些重要的模糊测试时,不要去尝试在同一个系统上禁用内存限制。

在 Nebula Graph 中开启 AddressSanitizer

我们在 Nebula Graph 中也使用了 AddressSanitizer,它帮助我们发现了非常多的问题。而在 Nebula Graph 中开启 AddressSanitizer 很简单,只需要在 Cmake 的时候带上打开 ENABLE_ASAN 这个 Option 就可以,比如:

Cmake -DENABLE_ASAN=On

这里建议所有的开发者在开发完毕功能运行单元测试的时候都打开 AddressSanitizer 来运行单元测试,这样可以发现很多不容易发现的内存问题,节省很多调试的时间。

附录

应用 AddressSanitizer 发现程序内存错误的更多相关文章

  1. AddressSanitizer —— ASAN分析内存错误

    简介 AddressSanitizer 是一个性能非常好的C/C++ 内存错误探测工具. 它由编译器的插桩模块和替换了malloc函数的运行时库组成. 这个工具可以探测如下这些类型的错误: 对堆.栈和 ...

  2. 问题-关于sharemem程序访问WEB出现内存错误处理

    [delphi技术] 关于sharemem造成dll错误的处理办法问题现象:如果程序和dll之间用string作为参数传递时容易出现错误问题处理:需要在程序的uses中使用sharemem.这个sha ...

  3. 如何快速定位找出SEGV内存错误的程序Bug

    通过查看php日志/usr/local/php/var/log/php-fpm.log,有如下警告信息: [16-Mar-2015 16:03:09] WARNING: [pool www] chil ...

  4. Adobe Acrobat 不能打开在线pdf。Adobe Acrobat 应用程序正在被终止,因为内存错误

    Adobe Acrobat 应用程序正在被终止,因为内存错误. Adobe Acrobat 不能打开在线pdf. 当出现上面两种错误时. 原因可能是Acrobat的更新有问题. 解决方法:打开C:\D ...

  5. Unix下C程序内存泄露检测工具:valgrind的安装使用

    Valgrind是一款用于内存调试.内存泄漏检测以及性能分析的软件开发工具. Valgrind的最初作者是Julian Seward,他于2006年由于在开发Valgrind上的工作获得了第二届Goo ...

  6. 小心DLL链接静态库时的内存错误

    本文转自http://www.bennychen.cn/2010/09/%E5%B0%8F%E5%BF%83dll%E9%93%BE%E6%8E%A5%E9%9D%99%E6%80%81%E5%BA% ...

  7. Linux mem 2.7 内存错误检测 (KASAN) 详解

    文章目录 1. 简介 2. Shadow 区域初始化 3. 权限的判断 3.1 read/write 3.2 memxxx() 4. 权限的设置 4.1 buddy 4.1.1 kasan_free_ ...

  8. C++程序内存泄漏检测方法

    一.前言 在Linux平台上有valgrind可以非常方便的帮助我们定位内存泄漏,因为Linux在开发领域的使用场景大多是跑服务器,再加上它的开源属性,相对而言,处理问题容易形成“统一”的标准.而在W ...

  9. Linux C程序内存空间

    linux下内存空间布置: 一个典型的Linux C程序内存空间由如下几部分组成: 代码段(.text).这里存放的是CPU要执行的指令.代码段是可共享的,相同的代码在内存中只会有一个拷贝,同时这个段 ...

随机推荐

  1. c数据结构 -- 使用链表实现计数

    #include <stdio.h> #include <stdlib.h> typedef struct _node{ int value; struct _node *ne ...

  2. 配置数据库属性validationQuery

    配置数据库时,属性validationQuery默认值为“select 1”,对于oracle值应为“select 1 from dual” validationQuery属性:用来验证数据库连接的语 ...

  3. 将java字节自动转为"B", "KB", "MB", "GB", "TB"等

    //字节转换 public static String readableFileSize(long size) { if (size <= 0) return "0"; fi ...

  4. 复制到粘贴板 && 提示

    copyText(String); TipLayer.showTip(String,timeOut);

  5. 爬山 启发式合并 / STL

    题目 其实 Kano 曾经到过由乃山,当然这名字一看山主就是 Yuno 嘛.当年 Kano 看见了由乃山,内心突然涌出了一股杜甫会当凌绝顶,一览众山小的豪气,于是毅然决定登山. 但是 Kano 总是习 ...

  6. 等差数列Arithmetic Progressions题解(USACO1.4)

    Arithmetic Progressions USACO1.4 An arithmetic progression is a sequence of the form a, a+b, a+2b, . ...

  7. D. Lunar New Year and a Wander bfs+优先队列

    D. Lunar New Year and a Wander bfs+优先队列 题意 给出一个图,从1点开始走,每个点至少要经过一次(可以很多次),每次经过一个没有走过的点就把他加到走过点序列中,问最 ...

  8. SpringBoot 各层之间的关系

    SpringBoot 各层之间的关系 SpringBoot 分为四层:controller层.service层.dao层.entity层.  entity层:和 model 层一样,存放的是实体类,属 ...

  9. 每天进步一点点------Allegro 铺铜、内电层分割

    一.Allegro 铺铜 1.建议初学者内电层用正片,因为这样就不用考虑flash焊盘,这时候所有的过孔和通孔该连内电层的就连到内电层,不该连的就不连.而如果用负片,那么如果做焊盘的时候如果没有做fl ...

  10. mongo shell远程连接使用数据库

    mongo mydb --username user1 --host --password --username 用户名 --host 连接ip --port 连接端口号 --password 密码 ...