RASPHINO'S BLOG
rasphino
Nov 12, 2018
阅读本文需要 21 分钟

Introduction

程序内存错误有很多种,比如内存访问错误segmentation fault、读取未初始化的数据、数组越界读/写、内存泄漏等等。这些内存错误通常很难通过传统的程序调试器追查出真正原因。最令人头疼的是内存错误有时不会立即导致程序崩溃,但是会使得程序的运行结果不可预料,这大大增加了我们 dubug 的难度。

在我之前的文章中(很遗憾现在已经没了),我曾介绍过名为 valgrind 的程序。valgrind 本质上是一个使用了即时编译JIT技术的虚拟机。简而言之,它会把程序“翻译”成临时的 IR 码,然后在 IR 码中插入 valgrind 的检测代码,最后将 IR 码转换回 CPU 可执行的机器码。 valgrind 简单易用,但是运行速度实在是太慢了,这导致 valgrind 可能无法检测并发条件下出现的 bug。

那有没有比 valgrind 更好用的工具呢?当然是有的啦!今天我要介绍就是功能比 valgrind 更丰富、运行速度更是快了 10 倍的 Sanitizers!


什么是 Sanitizers?

Sanitizers 是由 Google 开发的一系列检测器,包括 AddressSanitizer、MemorySanitizer、ThreadSanitizer、LeakSanitizer四个组件。目前该项目的代码已经并入 LLVM 仓库,并且被移植到了 GCC 中。自 clang 3.1 和 GCC 4.8 之后的版本都可以使用 Sanitizers,本文将使用 clang 作为编译器。


基本工作原理

之前已经提到 valgrind 需要把已经编译好的二进制文件动态翻译成 IR 码,那能不能直接在编译的环节就把测试代码加上呢?答案是肯定的,事实上这就是 Sanitizers 最基本的工作原理。


AddressSanitizer

用处

可检测程序的可寻址性问题,包括:

  • 堆、栈和全局变量的越界访问
  • 使用已经释放的内存use-after-free
  • 使用后返回use-after-return(需要添加ASAN_OPTIONS = detect_stack_use_after_return = 1
  • 在作用域外访问use-after-scope(需要添加-fsanitize-address-use-after-scope
  • 重复释放、无效释放内存
  • 内存泄露(实验性)

使用 AddressSanitizer 可能会导致程序的运行速度变慢 1 倍

使用方法

只需要在使用 clang 编译、链接代码时加上 -fsanitize=address flag。请确保使用 clang 而不是 ld 完成链接步骤,clang 会自动把 AddressSanitizer 库链接到最后生成的可执行文件中。

可选项

  • 为了提高性能,可以使用 -O1 或更高级的优化
  • 要使错误信息中的堆栈跟踪更美观,可以添加 -fno-omit-frame-pointer
  • 要获得完美的堆栈跟踪信息,请使用 -O1 优化并添加 -fno-optimize-sibling-calls 参数以禁用尾递归调用消除。
$ cat example_UseAfterFree.cpp
int main(int argc, char **argv) {
  int *array = new int[100];
  delete [] array;
  return array[argc];  // BOOM
}

$ clang++ -O1 -g -fsanitize=address -fno-omit-frame-pointer example_UseAfterFree.cpp

$ ./a.out
=================================================================
==4469==ERROR: AddressSanitizer: heap-use-after-free on address 0x61400000fe44 at pc 0x0000004ed7d2 bp 0x7ffffcc50680 sp 0x7ffffcc50678
READ of size 4 at 0x61400000fe44 thread T0
    #0 0x4ed7d1  (/home/rasp/a.out+0x4ed7d1)
    #1 0x7f417ef802e0  (/lib/x86_64-linux-gnu/libc.so.6+0x202e0)
    #2 0x41b2f9  (/home/rasp/a.out+0x41b2f9)

0x61400000fe44 is located 4 bytes inside of 400-byte region [0x61400000fe40,0x61400000ffd0)
freed by thread T0 here:
    #0 0x4eb0d0  (/home/rasp/a.out+0x4eb0d0)
    #1 0x4ed79e  (/home/rasp/a.out+0x4ed79e)
    #2 0x7f417ef802e0  (/lib/x86_64-linux-gnu/libc.so.6+0x202e0)

previously allocated by thread T0 here:
    #0 0x4eaad0  (/home/rasp/a.out+0x4eaad0)
    #1 0x4ed793  (/home/rasp/a.out+0x4ed793)
    #2 0x7f417ef802e0  (/lib/x86_64-linux-gnu/libc.so.6+0x202e0)

SUMMARY: AddressSanitizer: heap-use-after-free (/home/rasp/a.out+0x4ed7d1)
Shadow bytes around the buggy address:
  0x0c287fff9f70: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c287fff9f80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c287fff9f90: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c287fff9fa0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c287fff9fb0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x0c287fff9fc0: fa fa fa fa fa fa fa fa[fd]fd fd fd fd fd fd fd
  0x0c287fff9fd0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c287fff9fe0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c287fff9ff0: fd fd fd fd fd fd fd fd fd fd fa fa fa fa fa fa
  0x0c287fffa000: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c287fffa010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Heap right redzone:      fb
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack partial redzone:   f4
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==4469==ABORTING

好啦,接下来就是 windows 劝退环节(笑


MemorySanitizer

用处

可检测程序的未初始化内存使用问题

使用 MemorySanitizer 可能会导致程序的运行速度变慢 2 倍

使用方法

MemorySanitizer 只支持 Linux、NetBSD、FreeBSD 系统!(文档是这么写的,但是我觉得只要是 *UNIX 系统应该都支持)

和 AddressSanitizer 的使用方法基本一致,添加 -fsanitize=memory 即可。

可选项

  • 为了提高性能,可以使用 -O1 或更高级的优化
  • 要使错误信息中的堆栈跟踪更美观,可以添加 -fno-omit-frame-pointer
  • 要获得完美的堆栈跟踪信息,请使用 -O1 优化并添加 -fno-optimize-sibling-calls 参数以禁用尾递归调用消除。
$ cat umr.cpp
#include <stdio.h>
int main(int argc, char** argv) {
  int* a = new int[10];
  a[5] = 0;
  if (a[argc])
    printf("xx\n");
  return 0;
}

$ clang++ -fsanitize=memory -fno-omit-frame-pointer -g -O2 umr.cpp

$ ./a.out
==4543==WARNING: MemorySanitizer: use-of-uninitialized-value
    #0 0x48b89f  (/home/rasp/a.out+0x48b89f)
    #1 0x7fa998f802e0  (/lib/x86_64-linux-gnu/libc.so.6+0x202e0)
    #2 0x41c3b9  (/home/rasp/a.out+0x41c3b9)

SUMMARY: MemorySanitizer: use-of-uninitialized-value (/home/rasp/a.out+0x48b89f)
Exiting

ThreadSanitizer

用处

可检测 C++ 和 Go 程序的数据竞争和死锁问题

使用 ThreadSanitizer 可能会导致程序的运行速度变慢 5-15 倍、多占用 5-10 倍内存

使用方法

ThreadSanitizer 只支持 64 位 Linux、NetBSD、FreeBSD 系统!

和 AddressSanitizer 的使用方法基本一致,添加 -fsanitize=thread 即可。

可选项

  • 为了提高性能,可以使用 -O1 或更高级的优化
  • 为了得到文件名与行号,可以使用 -g
$ cat tiny_race.c
#include <pthread.h>
int Global;
void *Thread1(void *x) {
  Global = 42;
  return x;
}
int main() {
  pthread_t t;
  pthread_create(&t, NULL, Thread1, NULL);
  Global = 43;
  pthread_join(t, NULL);
  return Global;
}

$ clang -fsanitize=thread -g -O1 tiny_race.c

$ ./a.out
==================
WARNING: ThreadSanitizer: data race (pid=4553)
  Write of size 4 at 0x0000014aee78 by main thread:
    #0 main /home/rasp/t.c:10 (a.out+0x0000004a428e)

  Previous write of size 4 at 0x0000014aee78 by thread T1:
    #0 Thread1 /home/rasp/t.c:4 (a.out+0x0000004a4247)

  Location is global '<null>' at 0x000000000000 (a.out+0x0000014aee78)

  Thread T1 (tid=4555, finished) created by main thread at:
    #0 pthread_create <null> (a.out+0x000000424bd6)
    #1 main /home/rasp/t.c:9 (a.out+0x0000004a4284)

SUMMARY: ThreadSanitizer: data race /home/rasp/t.c:10 in main
==================
ThreadSanitizer: reported 1 warnings

LeakSanitizer

用处

用于检测内存泄漏问题

使用方法

LeakSanitizer 只支持 x86_64 架构上的 Linux 和 macOS 系统!

LeakSanitizer 已经整合在 AddressSanitizer 中,所以只要使用 -fsanitize=address 即可(如果只需要检测内存泄漏问题,也可以使用 -fsanitize=leak)。

$ cat memory-leak.c
#include <stdlib.h>
void *p;
int main() {
  p = malloc(7);
  p = 0; // The memory is leaked here.
  return 0;
}

$ clang -fsanitize=address -g memory-leak.c

$ ASAN_OPTIONS=detect_leaks=1 ./a.out
=================================================================
==4505==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 7 byte(s) in 1 object(s) allocated from:
    #0 0x4b9388  (/home/rasp/a.out+0x4b9388)
    #1 0x4ea39a  (/home/rasp/a.out+0x4ea39a)
    #2 0x7f9c685102e0  (/lib/x86_64-linux-gnu/libc.so.6+0x202e0)

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

其他工具

除了以上提到的四款工具,我在 clang 的文档中还发现了 DataFlowSanitizerUndefinedBehaviorSanitizer 这两个工具。感觉其中的 UndefinedBehaviorSanitizer 还挺有用的(专治谭浩强这种傻逼),但是迫于时间有限我就先不介绍了,有兴趣的朋友们可以自己看看文档。


Foot Note


Read More

AddressSanitizer 与 ThreadSanitizer 的实现方法

Sanitizer 的详细文档


小尾巴

  • 其实在 windows 上使用 wsl 也是可以使用这些工具的
  • 有什么想问的可以在下面留言(如果没有看到评论区,或者发现评论区有问题,请刷新页面)