写点什么

内存问题难定位,那是因为你没用 ASAN

  • 2022 年 8 月 05 日
  • 本文字数:2351 字

    阅读完需:约 8 分钟

内存问题难定位,那是因为你没用ASAN

本文分享自华为云社区《内存定位利器-ASAN使用小结》,作者:云存储开发者支持团队。

1.什么是 ASAN


ASAN 全称:Address Sanitizer,google 发明的一种内存地址错误检查器。目前已经被集成到各大编译器中。

2.为什么我们需要 ASAN


在 c/c++开发过程中,经常出现内存异常使用的问题,比如踩内存,被踩的内存如果未被使用对外无影响。而一旦使用了被踩的内存,可能会出现进程 core,死循环,进入异常分支等等各种千奇百怪的问题。这个时候要去定位这段内存为什么被踩,相当困难,因为已经错过了案发现场。如果不幸,遇到了这种问题,常用手段是:


1)分析被踩内存的特征值,比如是否是一个 magic 值,然后从代码库中找特征值,分析代码,缩小排查方向。


2)找到必现条件,通过 gdb 的 watch 功能,watch 被踩的内存地址,一旦被踩,gdb 将会打出踩内存的堆栈。


根据作者的经验,出现踩内存的问题需要消耗大量的人力定位。少则一人周,多种数人月。而这类问题,往往是由于某个低级编码错误引起的。


所以,我们迫切的希望,能在踩内存的第一现场就把凶手抓住,而不是在破坏已经表现出来的时候再去分析定位。而 asan 就能达到这个目的,它会接管内存的申请和释放,每次的内存的读写都会检查,因此可以做到快速的定位踩内存的问题。在 asan 之前也有其他的内存分析工具,但是 asan 是这些工具中比较优秀的,并不会损失大量的性能和内存(官方数据,性能下降两倍,而 valgrind 下降 20 倍:https://github.com/google/sanitizers/wiki/AddressSanitizerComparisonOfMemoryTools)。

3.ASAN 可以定位哪些内存使用问题


1、堆 OOB(堆内存越界)


int main(int argc, char **argv) {  int *array = new int[100]; array[0] = 0;  int res = array[argc + 100]; // BOOM delete [] array; return res;}
复制代码


2、栈外界栈越界


int main(int argc, char **argv) {  int stack_array[100]; stack_array[1] = 0; return stack_array[argc + 100]; // BOOM}
复制代码


3、Global OOB(GlobalOutOfBounds 全局变量越界)


int global_array[100] = {-1};int main(int argc, char **argv) { return global_array[argc + 100]; // BOOM}
复制代码


4、UAF(UseAfterFree 内存释放后使用)


int main(int argc, char **argv) {  int *array = new int[100]; delete [] array; return array[argc]; // BOOM}
复制代码


5、UAR(UseAfterReturn 栈内存回收后使用,该功能还存在少量 bug,默认未开启,开启 ASAN_OPTIONS=detect_stack_use_after_return=1)


int *ptr;__attribute__((noinline))void FunctionThatEscapesLocalObject() {  int local[100]; ptr = &local[0];}int main(int argc, char **argv) { FunctionThatEscapesLocalObject(); return ptr[argc];}
复制代码


6、UMR(uninitialized memory reads 读取未初始化内存)


7、Leaks(内存泄露)

4.怎么使用 ASAN 工具


现在大部分编译器已经集成了支持 asan 的能力,编译的时候加上编译选项即可。


常见的编译选项:


  • -fsanitize=address 开起 asan 能力,gcc 4.8 版本开启支持。

  • -fsanitize-recover=address :asan 检查到错误后,不 core 继续运行,需要配合环境变量 ASAN_OPTIONS=halt_on_error=0:report_path=xxx 使用。gcc 6 版本开始支持。


本文使用的是华为 EulerOS v2r9 版本。


下面开始我们的阿桑之旅


1、写个 bug,写一个释放后的内存还在使用的例子。


#include <stdlib.h>int main(){    int *p = malloc(sizeof(int)*10); free(p); *p = 3;//该程序正常情况下并不会导致进程core,因为free后的内存被glibc的内存分配器缓存着 return 0;}
复制代码


2、加上编译选项编译:gcc -fsanitize=address -g ./test.c -lasan -L /root/buildbox/gcc-10.2.0/lib64/ 其中-L 指定的是 libasan.so 存放的位置。


3、指定 asan 的 so 的目录,export LD_LIBRARY_PATH=/root/buildbox/gcc-10.2.0/lib64/,执行./a.out 执行程序,将可以看到 asan 报错。指出了内存异常使用的位置和原因。



4、在工程中,我们更希望程序遇到错误能不中断,而继续执行下去,我们可以使用 -fsanitize-recover=address 方法。这次我们更改下代码,多引入几个错误。


#include <stdlib.h>int main(){    int *p = malloc(sizeof(int)*10); free(p); *p = 3; //错误1.释放后继续使用    p = malloc(sizeof(int)*10);    p[11] = 3;//错误2,越界写 return 0;}
复制代码


5、编译:gcc -fsanitize=address -fsanitize-recover=address -g ./test.c -lasan -L /root/buildbox/gcc-10.2.0/lib64/


6、设置环境变量:export ASAN_OPTIONS=halt_on_error=0:log_path=/var/log/err.log,执行程序./a.out


7、查看日志路径:在/var/log 目录下,形成一个 err.log.212 的文件,212 是执行./a.out 的进程号。文件记录了详细的错误信息。



5. ASAN 的原理是什么


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


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


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


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


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


点击关注,第一时间了解华为云新鲜技术~

发布于: 刚刚阅读数: 3
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
内存问题难定位,那是因为你没用ASAN_云计算_华为云开发者联盟_InfoQ写作社区