0. 简介

2012 年,Google 发布了一篇论文:《AddressSanitizer: A Fast Address Sanity Checker》。介绍了一种新的内存检测方法,让上面的问题得到了很大的改进。截止到目前,AddressSanitizer 已经被广泛应用到了各种语言: C、C++、Objective-C、Java 等。以 Google 自己的 benchmark 为参考,开启 ASan 后,会使程序运行速度平均下降 2 倍左右,内存占用会增加 3 倍左右(和硬件有关)。这和其他工具动辄十几倍的消耗要好上不少。(如果要跑比较大的包,还是建议用AddressSanitzer的)。对于Address Sanitizer可以用来检测如下内存使用错误:

  • 内存释放后又被使用;
  • 内存重复释放;
  • 释放未申请的内存;
  • 使用栈内存作为函数返回值;
  • 使用了超出作用域的栈内存;
  • 内存越界访问;

1. AddressSanitzer是怎么做的

加了 ASAN 相关的编译选项后,代码中的每一次内存访问操作都会被编译器修改 为如下方式:

编译后:

程序中跑 Valgrind 其会极 大的降低程序运行速度,大约降低 10 倍,而跑 AddressSanitizer 大约只降低 2 倍! 与 valgrind 相比 asan 消耗非常低,甚至可以直接在生产环境中启用 asan 排查跟踪内存问题。

2. AddressSanitizer安装与配置

AddressSanitizer在gcc4.8后被默认加入,成为 gcc 的一部分,但不支持符号信息,无法显示出问题的函数和行数。从 4.9 开始,gcc 支持 AddressSanitizer 的 所有功能。因此 gcc 4.8 以上版本使用 ASAN 时不需要安装第三方库,通过在编译时指 定编译 CFLAGS 即可打开开关。

    如果使用 AddressSanitizer 时报错则需要先安装,Ubuntu 安装命令为:
sudo apt-get install libasan0

如果要使用命令行(gcc/g++),可以使用

  • -fsanitize=address:开启内存越界检测

  • -fsanitize-recover=address:一般后台程序为保证稳定性,不能遇到错误就简单退 出,而是继续运行,采用该选项支持内存出错之后程序继续运行,需要叠加设置 ASAN_OPTIONS= halt_on_error=0 才会生效;若未设置此选项,则内存出错即报错退出

  • -fno-stack-protector:去使能栈溢出保护

  • -fno-omit-frame-pointer:去使能栈溢出保护

  • -g1:表示最小调试信息,通常 debug 版本用-g 即-g2

如果要在CmakeList,则可以调用以下命令target_link_libraries:

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
set(CMAKE_LINKER_FLAGS "${CMAKE_LINKER_FLAGS} -fsanitize=address")
target_link_libraries(MyTarget
  asan
)

ASAN_OPTIONS 还需要完成环境变量设置选项,这样当程序出问题的时候我们可以立刻查到log情况:

halt_on_error=0:检测内存错误后继续运行

detect_leaks=1:使能内存泄露检测

malloc_context_size=15:内存错误发生时,显示的调用栈层数为 15

log_path=/home/asan.log:内存检查问题日志存放文件路径

export ASAN_OPTIONS=halt_on_error=0:use_sigaltstack=0:detect_leaks=1:malloc_context_size=15:log_path=/debug/asan.log

当出现问题后我们可以访问log文件,并记录了我们这个程序的错误是ERROR后面的内容 detected memory leaks,说明有内存泄漏。

错误类型有如下一些:

  • (heap) use after free 释放后使用

  • heap buffer overflow 堆缓存访问溢出

  • stack buffer overflow 栈缓存访问溢出

  • global buffer overflow 全局缓冲访问溢出

  • use after return

  • use after scope

  • initializations order bugs

  • memory leaks 内存泄露

1是内存分配的地方,#2是使用的地方。然后后面就是导致程序泄漏的位置,是一个进程的内存偏移形式,我们可以在编译时加-g选项,就可以定位出代码位置,例如:

3. 错误类型列举

3.1 Use after free

内存释放后还被使用。

int main(int argc, char **argv) {
  int *array = new int[100];
  delete [] array;
  return array[argc];  // BOOM
}
=================================================================
==3262==ERROR: AddressSanitizer: heap-use-after-free on address 0x614000000044 at pc 0x55c005566d89 bp 0x7fffc64dc040 sp 0x7fffc64dc030
READ of size 4 at 0x614000000044 thread T0
    #0 0x55c005566d88 in main /root/study/cmakeutils/src/main.cpp:6
    #1 0x7fdb76b17082 in __libc_start_main ../csu/libc-start.c:308
    #2 0x55c005566c4d in _start (/root/study/cmakeutils/build/main+0xdc4d)

0x614000000044 is located 4 bytes inside of 400-byte region [0x614000000040,0x6140000001d0)
freed by thread T0 here:
    #0 0x7fdb77396b97 in operator delete[](void*) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:163
    #1 0x55c005566d3c in main /root/study/cmakeutils/src/main.cpp:5
    #2 0x7fdb76b17082 in __libc_start_main ../csu/libc-start.c:308

previously allocated by thread T0 here:
    #0 0x7fdb77396097 in operator new[](unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:102
    #1 0x55c005566d25 in main /root/study/cmakeutils/src/main.cpp:4
    #2 0x7fdb76b17082 in __libc_start_main ../csu/libc-start.c:308
...

注:上述代码为 Bash

第 4 行显示了 READ(内存读取)的位置,return array[argc];

第 10 行显示了 freed(内存释放)的位置,delete [] array;

第 16 行显示了 allocate(内存申请)的位置,int *array = new int[100]。

3.2 Heap buffer overflow

申请了堆空间,数组下标超出申请范围。

int main(int argc, char **argv) {
  int *array = new int[100];
  array[0] = 0;
  int res = array[argc + 100];  // BOOM
  delete [] array;
  return res;
}
=================================================================
==3407==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6140000001d4 at pc 0x55753d9b4dbb bp 0x7ffe7d1e77e0 sp 0x7ffe7d1e77d0
READ of size 4 at 0x6140000001d4 thread T0
    #0 0x55753d9b4dba in main /root/study/cmakeutils/src/main.cpp:6
    #1 0x7f9f5683b082 in __libc_start_main ../csu/libc-start.c:308
    #2 0x55753d9b4c4d in _start (/root/study/cmakeutils/build/main+0xdc4d)

0x6140000001d4 is located 4 bytes to the right of 400-byte region [0x614000000040,0x6140000001d0)
allocated by thread T0 here:
    #0 0x7f9f570ba097 in operator new[](unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:102
    #1 0x55753d9b4d25 in main /root/study/cmakeutils/src/main.cpp:4
    #2 0x7f9f5683b082 in __libc_start_main ../csu/libc-start.c:308
...

注:上述代码为 Bash

第 4 行显示了 READ 的位置,int res = array[argc + 100];

第 11 行显示了 allocate 的位置,int *array = new int[100];

因为这里 argc + 100 >= 100, array 下标为 0-99,所以出现错误。

3.3 Stack buffer overflow

局部变量,数组下标超出范围。

int main(int argc, char **argv) {
  int stack_array[100];
  stack_array[1] = 0;
  return stack_array[argc + 100];  // BOOM
}
=================================================================
==3529==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fff4c128d44 at pc 0x55ccafbf0e13 bp 0x7fff4c128b60 sp 0x7fff4c128b50
READ of size 4 at 0x7fff4c128d44 thread T0
    #0 0x55ccafbf0e12 in main /root/study/cmakeutils/src/main.cpp:6
    #1 0x7f624dc97082 in __libc_start_main ../csu/libc-start.c:308
    #2 0x55ccafbf0c0d in _start (/root/study/cmakeutils/build/main+0xdc0d)

Address 0x7fff4c128d44 is located in stack of thread T0 at offset 452 in frame
    #0 0x55ccafbf0cd8 in main /root/study/cmakeutils/src/main.cpp:3

  This frame has 1 object(s):
    [48, 448) 'stack_array' (line 4) <== Memory access at offset 452 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
      (longjmp and C++ exceptions *are* supported)
...

注:上述代码为 Bash

第 4 行显示了 READ 的位置,return stack_array[argc + 100];

第 12 行显示了变量的位置,int stack_array[100]。

3.4 Global buffer overflow

全局变量,数组下标超出范围。

int global_array[100] = {-1};
int main(int argc, char **argv) {
  return global_array[argc + 100];  // BOOM
}
=================================================================
==3653==ERROR: AddressSanitizer: global-buffer-overflow on address 0x55b61f0391b4 at pc 0x55b61efd7d2b bp 0x7fff8bc1cbd0 sp 0x7fff8bc1cbc0
READ of size 4 at 0x55b61f0391b4 thread T0
    #0 0x55b61efd7d2a in main /root/study/cmakeutils/src/main.cpp:5
    #1 0x7f0637717082 in __libc_start_main ../csu/libc-start.c:308
    #2 0x55b61efd7c0d in _start (/root/study/cmakeutils/build/main+0xdc0d)

0x55b61f0391b4 is located 4 bytes to the right of global variable 'global_array' defined in '/root/study/cmakeutils/src/main.cpp:3:5' (0x55b61f039020) of size 400
...

注:上述代码为 Bash

第 4 行显示了 READ 的位置,return global_array[argc + 100];

第 8 行显示了全局变量的位置,int global_array[100] = {-1}。

3.5 Use after return

指针指向了一个函数的局部变量,函数返回后局部变量失效,但使用了该指针。

// 默认不检测该项,可设置ASAN_OPTIONS=detect_stack_use_after_return=1开启检测
int* ptr;
__attribute__((noinline)) void FunctionThatEscapesLocalObject() {
  int local[100];
  ptr = &local[0];
}

int main(int argc, char** argv) {
  FunctionThatEscapesLocalObject();
  return ptr[argc];
}
=================================================================
==3811==ERROR: AddressSanitizer: stack-use-after-return on address 0x7fd77133e234 at pc 0x555fb157be71 bp 0x7fffdb165710 sp 0x7fffdb165700
READ of size 4 at 0x7fd77133e234 thread T0
    #0 0x555fb157be70 in main /root/study/cmakeutils/src/main.cpp:11
    #1 0x7fd7746db082 in __libc_start_main ../csu/libc-start.c:308
    #2 0x555fb157bc0d in _start (/root/study/cmakeutils/build/main+0xdc0d)

Address 0x7fd77133e234 is located in stack of thread T0 at offset 52 in frame
    #0 0x555fb157bcd8 in FunctionThatEscapesLocalObject() /root/study/cmakeutils/src/main.cpp:4

  This frame has 1 object(s):
    [48, 448) 'local' (line 5) <== Memory access at offset 52 is inside this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
      (longjmp and C++ exceptions *are* supported)
...

注:上述代码为 Bash

第 4 行显示 READ 位置,return ptr[argc];

第 12 行显示变量位置,int local[100];

这里 ptr 指向了一个局部变量的地址 ptr = &local[0],然后在 FunctionThatEscapesLocalObject 函数返回后,访问该地址。

3.6 Use after scope

指针指向了一个范围变量,该范围退出后变量失效,但使用了该指针。

volatile int *p = 0;

int main() {
  {
    int x = 0;
    p = &x;
  }
  *p = 5;
  return 0;
}
=================================================================
==3922==ERROR: AddressSanitizer: stack-use-after-scope on address 0x7ffecd93f880 at pc 0x5616c0570de0 bp 0x7ffecd93f850 sp 0x7ffecd93f840
WRITE of size 4 at 0x7ffecd93f880 thread T0
    #0 0x5616c0570ddf in main /root/study/cmakeutils/src/main.cpp:10
    #1 0x7f2ccf8c3082 in __libc_start_main ../csu/libc-start.c:308
    #2 0x5616c0570c0d in _start (/root/study/cmakeutils/build/main+0xdc0d)

Address 0x7ffecd93f880 is located in stack of thread T0 at offset 32 in frame
    #0 0x5616c0570cd8 in main /root/study/cmakeutils/src/main.cpp:5

  This frame has 1 object(s):
    [32, 36) 'x' (line 7) <== Memory access at offset 32 is inside this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
      (longjmp and C++ exceptions *are* supported)
...

注:上述代码为 Bash

第 4 行显示了 WRITE(写内存)的位置,*p = 5;

第 12 行显示了变量位置,int x = 0;这里 x 的作用域在 {} 内部,由于 *p = 5 在 {} 外,所以 x 失效了。

3.7 Memory leaks

内存泄露,申请了未释放。

void *p;

int main() {
  p = malloc(7);
  p = 0; // The memory is leaked here.
  return 0;
}
=================================================================
==4076==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 7 byte(s) in 1 object(s) allocated from:
    #0 0x7f799fcff527 in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x55a10f15acfa in main /root/study/cmakeutils/src/main.cpp:6
    #2 0x7f799f482082 in __libc_start_main ../csu/libc-start.c:308

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

注:上述代码为 Visual Basic

第 6 行显示了内存申请的位置,p = malloc(7);

第 9 行显示了总的内存泄露。

4. 参考链接

https://www.jianshu.com/p/8f5830431266

https://xie.infoq.cn/article/d277a1c2e1123eeced6d5f1a8

https://www.bilibili.com/read/cv21072123

https://github.com/google/sanitizers/wiki/AddressSanitizerAlgorithm

https://blog.csdn.net/wowricky/article/details/115819820

https://mlog.club/article/178376

https://www.coder.work/article/2738155