by WANG
也许很多人都听说过类似这样的一个故事:某公司的服务器每隔3个月就会毫无预兆的崩溃一次,怎么也查不出原因,为了避免崩溃可能引发的问题,只得每2个月手动重启一次服务器。在这类有些灵异的事件背后,以内存泄露为代表的一系列内存错误往往就是那个幕后黑手。
在计算机科学中,内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏会导致可用内存的数量减少从而降低计算机的性能。过多的可用内存被分配掉会导致全部或部分设备停止正常工作,或者应用程序崩溃。
其他的内存错误还包括缓冲区溢出、越界访问等,此类错误轻则导致程序计算错误,重则引起程序崩溃,在内存紧张的嵌入式系统中尤为致命,Google旗下的开源工具AddressSanitizer可以帮助我们检测此类错误。
AddressSanitizer(ASan)是一个快速的内存错误检测工具。它非常快,只拖慢程序两倍左右。它包括一个编译器instrumentation模块和一个提供malloc()/free()替代项的运行时库。从gcc 4.8开始,AddressSanitizer成为gcc的一部分。
详细了解AddressSanitizer信息可以访问其github项目地址:
https://github.com/google/sanitizers/wiki/AddressSanitizer
AddressSanitizer主要包括两部分:插桩(Instrumentation)和动态运行库(Run-time library)。插桩主要是针对在llvm编译器级别对访问内存的操作(store,load,alloca等),将它们进行处理。动态运行库主要提供一些运行时的复杂的功能(比如poison/unpoison shadow memory)以及将malloc,free等系统调用函数hook住。该算法的思路是:如果想防住Buffer Overflow漏洞,只需要在每块内存区域右端(或两端,能防overflow和underflow)加一块区域(RedZone),使RedZone的区域的影子内存(Shadow Memory)设置为不可写即可。具体的示意图如下图所示。
AddressSanitizer保护的主要原理是对程序中的虚拟内存提供粗粒度的影子内存(每8个字节的内存对应一个字节的影子内存),为了减少overhead,采用了直接内存映射策略,所采用的具体策略如下:Shadow=(Mem >> 3) + offset。每8个字节的内存对应一个字节的影子内存,影子内存中每个字节存取一个数字k,如果k=0,则表示该影子内存对应的8个字节的内存都能访问,如果0<k<7,表示前k个字节可以访问,如果k为负数,不同的数字表示不同的错误(e.g. Stack buffer overflow, Heap buffer overflow)。
为了防止buffer overflow,需要将原来分配的内存两边分配额外的内存Redzone,并将这两边的内存加锁,设为不能访问状态,这样可以有效的防止buffer overflow(但不能杜绝buffer overflow)。以下是在栈中插桩的一个例子。
在动态运行库中将malloc/free函数进行了替换。在malloc函数中额外的分配了Redzone区域的内存,将与Redzone区域对应的影子内存加锁,主要的内存区域对应的影子内存不加锁。
free函数将所有分配的内存区域加锁,并放到了隔离区域的队列中(保证在一定的时间内不会再被malloc函数分配),可检测Use after free类的问题。
详细了解ASan算法原理可以访问以下地址:
https://github.com/google/sanitizers/wiki/AddressSanitizerAlgorithm
环境:Ubuntu 16.04 gcc 4.8以上
用-fsanitize=address选项编译和链接你的程序;
用-fno-omit-frame-pointer编译,以在错误消息中添加更好的堆栈跟踪。
增加-O1以获得更好的性能。
下面以简单的测试代码leaktest.c为例
在终端中输入以下命令编译leaktest.c
运行leaktest,会打印下面的错误信息:
第一部分(ERROR),指出错误类型detected memory leaks;
第二部分给出详细的错误信息:Direct leak of 80 byte(s) in 1 object(s),以及发生错误的对象名/源文件位置/行数;
第三部分是以上信息的一个总结(SUMMARY)。
可以根据以下的需求设置ASan检测白名单
忽略一个确定正确的函数以提高应用运行速度;
忽略一个特定功能的函数(例如:绕过框架边界穿过线程的堆栈);
忽略已知的问题。
使用no_sanitize_address属性(Clang3.3及GCC4.8以上版本支持)定义如下的宏命令: