AddressSanitizer使用总结及原理分析

AddressSanitizer

AddressSanitizer是一个内存工具,可以帮助我们快速发现进程中存在的内存问题,这对写C++人来说是一个神器。

特点

  • 高效,官方给出的说法是性能只降低一倍。实测我们游戏没感觉出来性能降低。
  • 受到官方编译编译器(gcc,clang)支持
  • 支持多个平台:
    • 完全支持的平台:linux,OS X,IOS模拟器环境,FreeBSD,Android
    • 部分支持的平台:windows(需要使用clang来编译),只支持 /MT /MD,并且不支持检测内存泄漏和use-after-return情况

可检测内容

  1. use after free:使用已经free的内存
  2. 各种(堆,栈,静态内存)内存溢出检测
  3. use after return/use after scope:使用不在作用于的内存
  4. 静态对象初始化顺序导致的问题
  5. 内存泄漏
  6. 申请和释放不匹配

使用方式

  • 加入编译选项-fsanitize=address,如果在问题出现时打印的堆栈比较清晰,可以加入-fno-omit-frame-pointer
  • use after return需要在启动命令前加入ASAN_OPTIONS=detect_stack_use_after_return=1
  • 静态对象初始化顺序需要在启动命令前加入ASAN_OPTIONS=check_initialization_order=true
  • 内存泄漏需要在启动命令前加入ASAN_OPTIONS=detect_leaks=1
  • 如果检测到问题推出的时候产生core,需要在启动命令前加入ASAN_OPTIONS=disable_coredump=0:unmap_shadow_on_exit=1:abort_on_error=1

原理

替换内存函数

  • malloc:在申请的内存周围插入内存(检测内存越界访问,如果越界的比较多就可能没办法检测出来)
  • free:将释放的内存放入一个隔离的列表中(检测释放内存被使用)

内存映射

将程序的虚拟内存分为两段:

  1. 程序内存:程序正常运行需要的内存
  2. shadown内存:用来记录程序内存是否有效,每8 bytes程序内存会映射到1 byte shadown内存。shadown内存的值有以下几种:
    • 0:所有byte都有效
    • 负数:所有byte都无效,全无效一般都是插入的内存,并且每种插入的内存对应的负数值不同
    • k:表示前k个byte是有效,后8-k个无效,前提:malloc返回的地址内存对齐

程序内存到shadown内存映射关系:

1
2
3
byte* MemToShadow(address) {
return (Mem >> 3) + 0x7fff8000; // 64位,32位 return (Mem >> 3) + 0x20000000;
}

插桩代码

AddressSanitizer会在所有访问内存的地方进行内存检测。类似于

1
2
3
4
5
6
7
8
// before
*address = ...; // or: ... = *address;

after:
if (IsPoisoned(address)) {
ReportError(address, kAccessSize, kIsWrite);
}
*address = ...; // or: ... = *address;

这也是所有内存工具的常规找问题的方式。难点是如果高效的检测这一块内存是否有效。上面介绍的内存映射可知AddressSanitizer的处理方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
byte *shadow_address = MemToShadow(address);
byte shadow_value = *shadow_address;
if (shadow_value) {
if (SlowPathCheck(shadow_value, address, kAccessSize)) {
ReportError(address, kAccessSize, kIsWrite);
}
}
*address = ...; // or: ... = *address;

// Check the cases where we access first k bytes of the qword
// and these k bytes are unpoisoned.
bool SlowPathCheck(shadow_value, address, kAccessSize) {
last_accessed_byte = (address & 7) + kAccessSize - 1;
return (last_accessed_byte >= shadow_value);
}

byte* MemToShadow(address) {
return (Mem >> 3) + 0x7fff8000; // 64位,32位 return (Mem >> 3) + 0x20000000;
}

实际的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void foo() {
char a[8];
...
return;
}

void foo() {
char redzone1[32]; // 32-byte aligned
char a[8]; // 32-byte aligned
char redzone2[24];
char redzone3[32]; // 32-byte aligned
int *shadow_base = MemToShadow(redzone1);
shadow_base[0] = 0xffffffff; // poison redzone1
shadow_base[1] = 0xffffff00; // poison redzone2, unpoison 'a'
shadow_base[2] = 0xffffffff; // poison redzone3
...
shadow_base[0] = shadow_base[1] = shadow_base[2] = 0; // unpoison all
return;
}

其他

  1. 默认情况下AddressSanitizer遇到问题会中断进程,但是AddressSanitizer提供了方式来避免这种情况(官方说这种方式目前在实验阶段,不是很可靠,并且第一次错误之后的错误可能是误报,所以不建议使用):
    • 编译选项加入:-fsanitize-recover=address,gcc 5.0之后版本才支持
    • 运行的时候加入:ASAN_OPTIONS=halt_on_error=0
  2. use after scope试了gcc 4.8和6.3两个版本,官方例子都没有出结果