Linux下内存检测工具:asan
1.GCC 版本 6.3
2.安装asan
yum install devtoolset-6-libasan-devel
3.注意
asan只是开发中使用工具,因此只能在debug模式下有效 不能用于release版本
介绍
首先,先介绍一下 Sanitizer 项目,该项目是谷歌出品的一个开源项目,该项目包含了 ASAN、LSAN、MSAN、TSAN等内存、线程错误的检测工具,这里简单介绍一下这几个工具的作用:
-
ASAN: 内存错误检测工具,在编译命令中添加
-fsanitize=address启用 -
LSAN: 内存泄漏检测工具,已经集成到 ASAN 中,可以通过设置环境变量
ASAN_OPTIONS=detect_leaks=0来关闭ASAN上的LSAN,也可以使用-fsanitize=leak编译选项代替-fsanitize=address来关闭ASAN的内存错误检测,只开启内存泄漏检查。 -
MSAN: 对程序中未初始化内存读取的检测工具,可以在编译命令中添加
-fsanitize=memory -fPIE -pie启用,还可以添加-fsanitize-memory-track-origins选项来追溯到创建内存的位置 -
TSAN: 对线程间数据竞争的检测工具,在编译命令中添加
-fsanitize=thread启用 其中ASAN就是我们今天要介绍的重头戏。
ASAN,全称 AddressSanitizer,可以用来检测内存问题,例如缓冲区溢出或对悬空指针的非法访问等。
根据谷歌的工程师介绍 ASAN 已经在 chromium 项目上检测出了300多个潜在的未知bug,而且在使用 ASAN 作为内存错误检测工具对程序性能损耗也是及其可观的。
根据检测结果显示可能导致性能降低2倍左右,比Valgrind(官方给的数据大概是降低10-50倍)快了一个数量级。
而且相比于Valgrind只能检查到堆内存的越界访问和悬空指针的访问,ASAN 不仅可以检测到堆内存的越界和悬空指针的访问,还能检测到栈和全局对象的越界访问。
这也是 ASAN 在众多内存检测工具的比较上出类拔萃的重要原因,基本上现在 C/C++ 项目都会使用ASAN来保证产品质量,尤其是大项目中更为需要。
1、编译选项
1.1 Gcc编译选项
# -fsanitize=address:开启内存越界检测
# -fsanitize-recover=address:一般后台程序为保证稳定性,不能遇到错误就简单退出,而是继续运行,采用该选项支持内存出错之后程序继续运行,需要叠加设置ASAN_OPTIONS=halt_on_error=0才会生效;若未设置此选项,则内存出错即报错退出
ASAN_CFLAGS += -fsanitize=address -fsanitize-recover=address
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS } -fsanitize=address -fsanitize-recover=address")
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
ASAN 的基本原理
ASAN的内存检测方法与Valgrind的AddrCheck工具很像,都是使用shadow内存来记录应用程序的每个字节是否可以被安全的访问,在访问内存时都对其映射的shadow内存进行检查。但是,
ASAN使用一个更具效率的shadow内存映射机制和更加紧凑的内存编码来实现,并且除了堆内存外还能检测栈和全局对象中的错误访问,且比AddrCheck快一个数量级。
ASAN由两部分组成:代码插桩模块和运行时库。
代码插桩模块会修改代码使其在访问内存时检查每块内存访问状态,称为
shadow 状态,以及在内存两侧创建redzone的内存区域。运行时库则提供一组接口用来替代
malloc和free以及相关的函数,使得在分配堆空间时在其周围创建redzone,并在内存出错时报告错误。首先,我们先介绍一下什么是
shadow 内存和redzone。
shadow 内存
在
ASAN中malloc函数返回的内存地址通常至少是8个字节对齐,比如malloc(15)将分配得到2块大小为8字节的内存,在这个场景中,第二块8字节内存的前5个字节是可以访问,但剩下的3个字节是不能访问的。所谓的
shadow 内存就是在应用程序的虚拟地址空间中预留一段地址空间,用来存储映射应用程序访问的内存块中哪些字节可以被使用的信息,这些信息就是shadow 状态。其中每1个字节的shadow 内存,映射到8个字节的应用程序内存,因此,shadow状态可能有3种:
ASAN使用带有比例和偏移量的直接映射将应用程序地址转换为其对应的shadow内存地址:shadow_address = (addr >> 3) + offset假设
max - 1是虚拟地址空间中的最大有效地址,则offset的值应选择为在启动时不被占用的从offset到offset+Max/8的区域。以下是 32 位 linux 系统中的地址空间分布
0x1 0000 0000 --------------- | HIGH | | MEMORY | 0x4000 0000 --------------- | HIGH SHADOW | 0x2800 0000 --------------- | BAD REGION | 0x2400 0000 --------------- | LOW SHADOW | 0x2000 0000 --------------- | LOW MEMORY | 0x0000 0000 ---------------虚拟地址空间被划分为高低两部分,每个部分的内存地址映射到相应的
shadow 内存。注意:将shadow 内存中的地址进行映射会得到Bad 区域中的地址,Bad 区域是被页面保护标记为不可访问的地址空间。
shadow映射方式可以推导为(addr >> scale) + offset的形式,其中scale是的取值范围是1~7,当scale=N时,shadow 内存占用虚拟地址空间的1/2^N,red-zone的最小大小为2^N字节(保证malloc()的对齐要求)。shadow 内存中的每个字节描述了2^N个内存字节的状态并有2^N + 1个不同的值。
在 32 位 linux 系统中,虚拟地址空间为:
0x00000000-0xffffffff,offset = 0x20000000(2^29)。在 64 位系统中,
ofsset = 0x0000100000000000(2^44)。在某些情况下(例如,在 Linux 上使用
-fPIE/-pie编译器标志)可以使用零偏移来进一步简化检测。
0: 表示映射的
8个字节均可以使用k(1<=k<=7): 表示表示映射的8个字节中只有前
k个字节可以使用负值: 表示映射的
8个字节均不可使用,且不同的值表示所映射不同的内存类型(堆、栈、全局对象或已释放内存)redzone
ASAN会在应用程序使用的堆、栈、全局对象的内存周围分配额外内存,这个额外的内存叫做redzone,redzone会被shadow 内存标记为不可使用状态,当应用程序访问redzone内存时说明已经溢出访问了,此时,ASAN检测redzone的shadow 状态后就会报告相应错误。readzone越大,检测内存下溢和上溢的范围越大。具体的分配策略将在下面涉及。代码插桩
ASAN会在应用程序访问内存的位置进行插桩,对于访问完整8字节内存的位置,插入以下代码检查内存对应的 shadow 内存,以此判断是否访问异常:
ShadowAddr = (Addr >> 3) + Offset; if (*ShadowAddr != 0) ReportAndCrash(Addr);由于应用程序访问8字节的内存,因此,其映射的
shadow 内存的存储值必须是0,表示该8字节内存完全可用,否则,报错。应用程序对 1、2、或者 4 字节内存的访问要复杂一些,如果访问的内存块对应的
shadow 内存的存储值如果不是负数,且不为0,或者将要访问内存块超过了shadow 内存表示的可用范围,意味着本次将访问到不可使用的内存:
ShadowAddr = (Addr >> 3) + Offset; k = *ShadowAddr; if (k != 0 && ((Addr & 7) + AccessSize > k)) ReportAndCrash(Addr);需要注意的是,
ASAN对源代码的插桩时机是在LLVM对代码编译优化之后,也就意味着ASAN只能检测LLVM优化后幸存下来的内存访问,例如:被LLVM优化掉的对栈对象进行访问的代码将不会被ASAN所识别。同时,
ASAN也不会对LLVM生成的内存访问代码进行插桩,例如:寄存器溢出检查等等。另外,即使错误报告代码
ReportAndCrash(Addr)只会被调用一次,但由于会在代码中的许多位置进行插入,因此,错误报告代码也必须相当紧凑。目前
ASAN使用了一个简单的函数调用来处理错误报告,当然还有另一个选择是插入一个硬件异常。运行时库
在应用程序启动时,将映射整个
shadow 内存,因此程序的其他部分不能使用它。BAD 区域也是受保护的,应用程序也不能访问。在 linux 操作系统中,
shadow 内存区域不会被占用,因此,映射总是成功的。但在 MacOS 中可能需要禁用地址空间布局(ASLR)。另外,根据 GOOGLE 工程师介绍,
shadow 内存区域的布局也适用于 windows 操作系统。启用
ASAN时,源代码中的malloc和free函数将会被替换为运行时库中的malloc和free函数。
malloc分配的内存区域被组织为为一个与对象大小相对应的空闲列表数组。当对应于所请求内存大小的空闲列表为空时,从操作系统(例如,使用mmap)分配带有redzone的内存区域。n个内存块,将分配n+1个redzone:| redzone-1 | memory-1 | redzone-2 | memory-2 | redzone-3 |
free函数会将整个内存区域置成不可使用并将其放入隔离区,这样该区域就不会马上被malloc分配给应用程序。目前,隔离区是使用一个 FIFO 队列实现的,它在任何时候都拥有一定数量的内存。
默认情况下,
malloc和free记录当前调用堆栈,以便提供更多信息的错误报告。malloc调用堆栈存储在左侧redzone中(redzone越大,可以存储的帧数越多),而free调用堆栈存储在内存区域本身的开头。到这里你应该已经明白了对于动态分配的内存,
ASAN是怎么实现检测的,但你可能会产生疑惑:动态分配是通过malloc函数分配redzone来支持错误检测,那栈对象和全局对象这类没有malloc分类内存的对象是怎么实现的呢?其实原理也很简单:
对于全局变量,
redzone在编译时创建,redzone的地址在应用程序启动时传递给运行时库。运行时库函数会将redzone设置为不可使用并记录地址以供进一步错误报告。
对于栈对象,
redzone是在运行时创建和置为不可使用。目前,使用32字节的redzone。例如以下代码片段:
void foo() { char a[10]; <function body> } 经ASAN处理后的代码大致如下:
void foo() { char rz1[32] char arr[10]; char rz2[32-10+32]; unsigned * shadow = (unsigned*)(((long)rz1>>8)+Offset); // 将 redzone 设置为不可使用 shadow[0] = 0xffffffff; // rz1 shadow[1] = 0xffff0200; // arr and rz2 shadow[2] = 0xffffffff; // rz2 <function body> // 将所有内存设置成可以使用 shadow[0] = shadow[1] = shadow[2] = 0; }总结
ASAN 使用
shadow 内存和redzone来提供准确和即时的错误检测。传统观点认为,
shadow 内存和redzone要么通过多级映射方案产生高开销,要么占用大量的程序内存。但,ASAN的使用的shadow映射机制和shadow 状态编码减少了对内存空间占用。最后,如果你觉得
ASAN插桩代码和检测的对你某些的代码来说太慢了,那么可以使用编译器标志来禁用特定函数的,使ASAN跳过对代码中某个函数的插桩和检测, 跳过分析函数的编译器指令是:__attribute__((no_sanitize_address))