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

3A网络
• 阅读 567

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

1. 什么是 ASAN

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

2. 为什么我们需要 ASAN

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

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

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

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

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

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

1、Heap OOB(HeapOutOfBounds 堆内存越界)

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、Stack OOB(StackOutOfBounds 栈越界)

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 版本。

下面开始我们的 asan 之旅

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 报错。指出了内存异常使用的位置和原因。

内存问题难定位,那是因为你没用 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 的进程号。文件记录了详细的错误信息。

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

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

5. ASAN 的原理是什么

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

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

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

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

感兴趣的伙伴可以在3A云服务器上部署相关环境进行测试,计算影子内存的地址需要快速,他们采用了:主内存地址除以 8,再加上一个偏移量的做法。因为堆栈分别在虚拟内存地址空间的两端,这样影子内存就会落在中间。而如果用户以外访问了影子内存,那么影子内存的 "影子内存" 就会落到一个非法的范围 (Shadow Gap) 内,就可以知道访问出了些问题。

点赞
收藏
评论区
推荐文章
爱库里 爱库里
3年前
图文并茂讲清楚 JavaScript 内存管理
作为一个JavaScript的开发者,大多数情况下你可能不会担心内存管理问题,因为JavaScript引擎会帮你处理这些。但是在开发过程中,你或多或少的会遇到一些相关的问题,比如内存泄漏等,只有了解了内存分配的工作机制,你才会知道如何去解决这些问题。在这篇文章中,我将会向你介绍内存分配和垃圾收集的机制,以及如何避免一些常见的内存泄漏的
深入了解 JavaScript 内存泄漏
在任何语言开发的过程中,对于内存的管理都非常重要,JavaScript也不例外。但是如果我们对内存泄漏没有什么概念,就有可能因为内存泄漏,导致许多问题。了解内存泄漏,如何避免内存泄漏,都是不可缺少的。
浪人 浪人
3年前
Android 内存泄露:详解 Handler 内存泄露的原因与解决方案
前言在Android开发中,内存泄露十分常见1.内存泄露的定义:本该被回收的对象不能被回收而停留在堆内存中2.内存泄露出现的原因:当一个对象已经不再被使用时,本该被回收但却因为有另外一个正在使用的对象持有它的引用从而导致它不能被回收。这就导致了内存泄漏。本文将详细讲解内存泄露的其中一种情况:在Handler中发生的内
记住几种出现内存泄漏的点
Android内存优化——常见内存泄露及优化方案如果一个无用对象(不需要再使用的对象)仍然被其他对象持有引用,造成该对象无法被系统回收,以致该对象在堆中所占用的内存单元无法被释放而造成内存空间浪费,这中情况就是内存泄露。在Android开发中,一些不好的编程习惯会导致我们的开发的app存在内存泄露的情况。下面介绍一些在Android开发中常见的内存泄
Stella981 Stella981
2年前
Android内存溢出分析
   内存溢出,是Android开发中常遇到的问题,解决起来总是摸不着头脑。今天爬爬就来讲讲如何定位内存溢出。OOM(内存溢出)和MemoryLeak(内存泄露)有什么关系?OOM可能是因为MemoryLeak,也可能是你的应用本身就比较耗内存(比如图片浏览型的,或者应用本身的
Stella981 Stella981
2年前
Android Native 内存泄漏系统化解决方案
导读:C内存泄漏问题的分析、定位一直是Android平台上困扰开发人员的难题。因为地图渲染、导航等核心功能对性能要求很高,高德地图APP中存在大量的C代码。解决这个问题对于产品质量尤为重要和关键,高德技术团队在实践中形成了一套自己的解决方案。分析和定位内存泄漏问题的核心在于分配函数的统计和栈回溯。如果只知道内存分配点不知道调用栈会
Wesley13 Wesley13
2年前
Java堆外内存排查小结
!timg.jpeg(http://sayhiai.com/usr/uploads/2018/06/1660937585.jpeg)简介JVM堆外内存难排查但经常会出现问题,这可能是目前最全的JVM堆外内存排查思路。通过本文,你应该了解:pmap命令gdb命令perf命令内存RSS、
Stella981 Stella981
2年前
Javascript内存泄露
1.什么是内存泄露?内存泄露是指分配给应用的内存不能被重新分配,即使在内存已经不被使用的时候。正常情况下,垃圾回收器在DOM元素和event处理器不被引用或访问的时候回收它们。但是,IE的早些版本(IE7和之前)中内存泄露是很容易出现的,因为内存管理器不能正确理解Javascript生命周期而且在周期被打破(可以通过赋值为null实现)前不会回收
Wesley13 Wesley13
2年前
.NET陷阱之五:奇怪的OutOfMemoryException——大对象堆引起的问题与对策
我们在开发过程中曾经遇到过一个奇怪的问题:当软件加载了很多比较大规模的数据后,会偶尔出现OutOfMemoryException异常,但通过内存检查工具却发现还有很多可用内存。于是我们怀疑是可用内存总量充足,但却没有足够的连续内存了——也就是说存在很多未分配的内存空隙。但不是说.NET运行时的垃圾收集器会压缩使用中的内存,从而使已经释放的内存空隙连成一片吗?
Stella981 Stella981
2年前
JavaScript:垃圾收集机制
  JavaScript具有自动垃圾收集机制。也就是说,执行环境会负责管理代码执行过程中使用的内存。开发人员不必关心内存分配和回收问题。  垃圾收集机制的原理:找到不再继续使用的变量,然后进行释放其占用的内存。所以,垃圾收集器会按照固定的时间间隔(或代码执行中设定的收集时间)持续执行这一操作。  垃圾收集器会跟踪哪些变量有用哪些变量没用,对没用的变量