菜鸟笔记
提升您的技术认知

Asan内存检测工具

阅读 : 2652

Google 发明了 Address Sanitizer, 是一种地址错误检查器,asan(Address-Sanitizier)早先是LLVM中的特性,后被加入GCC 4.8,在GCC 4.9后加入对ARM平台的支持。因此GCC 4.8以上版本使用ASAN时不需要安装第三方库,通过在编译时指定编译CFLAGS即可打开开关。

它可以检测出下面这些错误:

Use after free (dangling pointer dereference)
堆上分配的空间被 free 之后再次使用(指针解引用).

Heap buffer overflow
访问的区域在堆上, 并且超过了分配的空间.

Stack buffer overflow
访问的区域在栈上, 并且超过了分配给它的空间.

Global buffer overflow
访问的区域是全局变量, 并且超过了分配给它的空间.

Use after return
默认不开启, 指: 函数在栈上的局部变量在函数返回后被使用.

Use after scope
局部变量离开作用域以后继续使用.

Initialization order bugs
默认不开启. 检查全局变量或静态变量初始化的时候有没有利用未初始化的变量.

Memory leaks
程序结束时未释放堆上分配的内存.

原理简介
首先我们需要接管每次内存分配/释放. 并且每一次对内存的读/写都需要加上一个检查 (所以需要编译器的配合).

对于上面这些需要检测出的问题, ASan 提出了解决方案, 可以比较好的处理这些问题, 同时不至于损失太多性能/空间.

影子内存
我们要记录每一块内存的可用性. 把用户程序所在的内存区域叫做主内存, 而记录主内存可用性的内存区域, 则叫做影子内存 (Shadow memory).

所有主内存的分配都按照 8 字节的方式对齐. 然后按照 1:8 的压缩比例对主内存的可用性进行记录, 然后存入影子内存中. 影子内存无法被用户直接读写, 需要编译器生成相关的代码来访问.

每一次内存的分配和释放, 都会写入影子内存. 每次读/写内存区域前, 都会读取一下影子内存, 获得这块内存访问合法性 (是否被分配, 是否已被释放).

对影子内存的写入只在分配内存的时候发生, 所以只要分配内存是多线程安全的, ASan 就是多线程安全的, 这在大部分情况下也确实成立.

计算影子内存的地址需要快速, 他们采用了: 主内存地址除以 8, 再加上一个偏移量的做法. 因为堆栈分别在虚拟内存地址空间的两端, 这样影子内存就会落在中间. 而如果用户以外访问了影子内存, 那么影子内存的"影子内存"就会落到一个非法的范围 (Shadow Gap) 内, 就可以知道访问出了些问题.

投毒
为了禁止使用某些内存地址, 我们可以在影子内存里下毒 (poisoning). 影子内存里面每个 byte, 都记录了其对应的 8-byte 内存的下毒情况. 由于我们要求内存分配是按照 8 字节对齐的, 所以主内存里面如果有毒, 一定是前面连续 字节无毒, 后面连续 字节有毒. 这个就会记录在影子内存里. 之后具体功能的实现就很好办了.

Use after free (dangling pointer dereference)
free 掉的区域马上变成有毒的即可. 当然, 过了一段时间可以给它解毒. 他会持有一段 FIFO 的有毒内存队列, 默认大小是 256 MB. 所以如果你 Use after free 在 256 MB 新的分配之后, 也有可能检测不出来.

Buffer overflow
这几种的 buffer overflow 的实现方案都是在每一个对象前后下毒, 用毒把大家包围. 只要超出了访问区域就会遇到下毒的区域, 叫做红区(red zone). 红区的长度很有可能至少是 32 字节(对应四个影子内存的 byte), 并且保持 32 字节对齐.

可以看出, 由于具有 1:8 的压缩比, 并且是按照 8 字节对齐的, 如果访问的内存区域未对齐, 则有可能检测不出越界访问. 比如这个例子.

同时也可以看出, 只有对象前后的局部区域不能访问, 其余地方都是可以访问的. 如果这个访问恰巧完整落在另外一个对象里面, 那就找不到这个错误.

Use after scope/return
离开了局部作用域就把对应内存下毒即可. 由于默认情况下不检测 use after return, 所以可以在 return 的时候就对这段内存解毒.

Memory leaks
程序结束时检测堆上未释放的内存, 报个错. 这个实现起来并不复杂. 还可以检测出二次释放等问题.

因为我们在堆上分配了 red zone, 这些 red zone 也可以利用起来, 写入一些调试信息在里面. 这样可以知道啥地方出了问题.

使用

1、编译选项

1.1 Gcc编译选项

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

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

ASAN_CFLAGS += -fsanitize=address -fsanitize-recover=address

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

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

# -fno-var-tracking:默认选项为-fvar-tracking,会导致运行非常慢

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

ASAN_CFLAGS += -fno-stack-protector -fno-omit-frame-pointer -fno-var-tracking -g1

1.2 Ld链接选项

ASAN_LDFLAGS += -fsanitize=address -g1

如果使用gcc链接,此处可忽略。

2、ASAN运行选项

2.1 ASAN_OPTIONS设置

ASAN_OPTIONS是Address-Sanitizier的运行选项环境变量。

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

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

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

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

# suppressions=$SUPP_FILE:屏蔽打印某些内存错误

export ASAN_OPTIONS=halt_on_error=0:use_sigaltstack=0:detect_leaks=1:malloc_context_size=15:log_path=/home/xos/asan.log:suppressions=$SUPP_FILE

除了上述常用选项,以下还有一些选项可根据实际需要添加:

# detect_stack_use_after_return=1:检查访问指向已被释放的栈空间

# handle_segv=1:处理段错误;也可以添加handle_sigill=1处理SIGILL信号

# quarantine_size=4194304:内存cache可缓存free内存大小4M

ASAN_OPTIONS=${ASAN_OPTIONS}:verbosity=0:handle_segv=1:allow_user_segv_handler=1:detect_stack_use_after_return=1:fast_unwind_on_fatal=1:fast_unwind_on_check=1:fast_unwind_on_malloc=1:quarantine_size=4194304

2.2 LSAN_OPTIONS设置

LSAN_OPTIONS是LeakSanitizier运行选项的环境变量,而LeakSanitizier是ASAN的内存泄漏检测模块,常用运行选项有:

# exitcode=0:设置内存泄露退出码为0,默认情况内存泄露退出码0x16

# use_unaligned=4:4字节对齐

export LSAN_OPTIONS=exitcode=0:use_unaligned=4

3、总结

实际开发环境中,可能存在gcc版本低,使用asan做内存检查时,需要链接libasan.so库的情况。其次,平台软件通常都会内部实现一套内存操作接口,为使用asan工具,需要替换成glibc提供的接口。此时,可以通过LD_PRELOAD环境变量解决这类问题。

export LD_PRELOAD= libasan.so.2:libprelib.so   #vos_malloc --> malloc