内存检测工具
简介
实际在 ASan 出现之前,市面上就已经存在了许多内存检测器,如
- Dr.Memory 检测未初始化的内存访问、double free、use after free 等错误
- Mudflap 检测指针的解引用,静态插桩
- Insure++ 检测内存泄漏
- Valgrind 可以检测非常多的内存错误
但是无一例外,Dr.Memory、Insure++ 和 Mudflap 虽然在运行时造成的额外损耗比较少,但是检测场景有限;
内存调试工具大致分为 3 种类型:
填充字节方法
最常用的方法是在每个内存块的开头和末尾添加额外的填充字节。调试工具会在特定的位置(通常是在内存分配 API 入口。例如 malloc 和 free 函数)检查这些填充字节。
DieHarder, Dmalloc为代表,分配内存时,在被分配内存的前后,额外分配内存,并填充特殊的值,释放内存的时候,在被释放的内存里也填充特殊值。如果程序读到了这些特殊值,就表示程序访问内存越界了。
这种方法的缺点是,无法及时检测到越界访问行为,只能在运行结束时分析特殊值是否被读取或改写来计算总结,这会导致一定的概率检测不到错误。
系统保护页
这类工具会利用CPU的内存页保护功能,在可能被越界的内存块前后设置一个不可访问的系统保护页。当程序试图非法访问受保护的内存时,系统会通过硬件检测到这些操作,并在内存访问指令出停止执行程序。
以 Electric Fence, Duma, GuardMalloc, Page Heap为代表的工具,使用 CPU 的内存页保护功能:CPU 访问一个不可访问的内存页的时候,会触发异常。该类工具实现的内存分配函数,除了正常的分配内存,还在后面紧接着分配一个不可访问的内存页。程序如果访问内存越界,就访问到了后面的内存页,触发异常。
这种办法的缺点是,如果程序需要分配很多的内存,会导致分配很多后面的不可访问的内存页,分配内存次数多,就会运行的很慢。并且这种方式无法检测到轻微的越界,比如分配了5个字节的内存,访问了第 6个字节,因为内存对齐的原因,访问第 6 个字节的内存不会触发异常。
动态二进制分析
基于 Valgrind 的工具就是这种类型的主要代表。 Valgrind 是一个动态二进制检测框架,可以运行任何现有的程序而无须重新编译,它的 Memcheck 插件工具旨在检测各种内存错误,包括无效访问和内存泄漏。Memcheck 在内存使用影子内存
来跟踪程序的内存使用情况,每次内存访问都会更新影子内存。
缺点:由于细粒度和软件模式检查,性能可能会下降
Valgrind虽然能够在许多场景的检测出错误,但是它实现了自己的一套ISA并在其之上运行目标程序,因此它会严重拖慢目标程序的速度。而ASan在设计时就综合考虑了检测场景、速度的影响因素,结合了 Mudflap 的静态插桩、Valgrind的多场景检测。
ASan 由两部分组成:一个是静态插桩模块,将内存访问判断的逻辑直接插入在了二进制中,保证了检测逻辑的执行速度;另一部分则是运行时库,提供部分功能的开启、报错函数和 malloc/free/memcpy 等函数的 asan 检测版本。
谷歌的地址消毒器(Google Address Sanitizer),与Valgrind相似,采用影子内存来跟踪程序的实时内存使用。不同之处在于,GAS 通过编译器在生成的二进制文件中插入诊断代码,因此它不需要二进制检测框架,也就是 Valgrind 的CPU 模拟器。由于实现的不同,GAS的平均性能损耗在 2 倍左右,而ValGrind 在 10-20倍。
间接映射的代表是 valgrind 和 Dr.Memory。他们设置多个影子内存段,然后配合查表法来完成映射。
基本上都是使用专用版本的内存分配和释放函数,替换系统的内存分配和释放函数,由此提供额外的内存管理功能,检测内存的异常使用,同时又不改变原来程序的流程。
基本上,除了ASAN,都只能用来检测堆内存上的问题。但是StackGuard 和 Propolice 利用同样的原理,在栈上面也填充一些特殊值,在程序返回的时候检测是否被改写,来发现问题。
实际的内存检测工具,往往多种技术并用,在细节上,算法上有所差异,导致工具的性能和准确度各有千秋。通常检测质量高的,效率比较低;效率高的,质量又会低。有的工具,会吃掉数倍甚至数十倍的内存,cpu 效率也降低到1/10的量级。Address Sanitizer 在多种工具的基础上,各取所长,显著提高质量和效率,综合只有 73% 的降低。