当前位置: 首页 > news >正文

内存问题排查工具ASan初探

在复杂软件开发中,代码量大,往往由多人进行协同开发,当遇到内存使用不当导致程序崩溃时,通过代码走读、加打印等人工排查的方法,可能很难定位问题,如果能借助一些工具来排查,也许能有事半功倍的效果。

本篇就来介绍ASan这款内存错误检测工具,来帮助分析内存使用的相关问题。

1 ASan简介

ASan(AddressSanitizer),是 Google 开发的一种内存错误检测工具,广泛用于 C、C++ 等语言的代码中,主要用于检测和调试内存相关问题,如:使用未分配的内存、使用已释放的内存、堆内存溢出等。

ASan在编译时将额外的代码插入到目标程序中,对内存的读写操作进行检测和记录。

在程序运行时,ASan会监测内存访问,一旦发现内存访问错误,会立即输出错误信息并中断程序执行,并提供详细报告帮助开发者定位问题。

LeakSanitizer(内存泄漏检测器)是集成在 AddressSanitizer中的内存泄漏检测工具。

2 使用

2.1 基本使用(-fsanitize=address)

使用支持 ASan 的编译器,如GCC或Clang,并开启ASan相关编译选项。

gcc -fsanitize=address your_code.c -g -o your_program

编译后,运行代码,当ASan检测到代码有相应的内存问题,就会结束程序,并给出类似如下的错误信息

=================================================================
==8950==ERROR: LeakSanitizer: detected memory leaksDirect leak of 20 byte(s) in 1 object(s) allocated from:#0 0x7f4fed3a8acb in __interceptor_malloc (/usr/lib/x86_64-linux-gnu/liblsan.so.0+0xeacb)#1 0x55ed2291e7d2 in main /home/xxpcb/myTest/linux/ASan/MemLeak/test1.cpp:9#2 0x7f4fecfcac86 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21c86)Direct leak of 4 byte(s) in 1 object(s) allocated from:#0 0x7f4fed3a9c3b in operator new(unsigned long) (/usr/lib/x86_64-linux-gnu/liblsan.so.0+0xfc3b)#1 0x55ed2291e7e0 in main /home/xxpcb/myTest/linux/ASan/MemLeak/test1.cpp:11#2 0x7f4fecfcac86 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21c86)SUMMARY: LeakSanitizer: 24 byte(s) leaked in 2 allocation(s).

2.2 -fsanitize的其它参数

除了-fsanitize=address,GCC 中还有许多类似的参数,用于检测不同类型的程序错误:

  • -fsanitize=leak:用于检测内存泄漏

    该功能其实已集成到 AddressSanitizer 中,使用-fsanitize=address也能检测内存泄漏,不过使用此参数可单独关闭 ASAN 的其他内存错误检测,只专注于内存泄漏检查。

  • -fsanitize=thread:开启 ThreadSanitizer,用于检测多线程程序中的数据竞争和死锁等问题。

    需要注意的是,它不能与-fsanitize=address-fsanitize=leak共用。

  • -fsanitize=undefined:开启 UndefinedBehaviorSanitizer,可检测程序中的未定义行为

    如除零错误、访问越界数组、未初始化变量的使用等。

  • -fsanitize=memory:开启 MemorySanitizer,主要用于检测未初始化内存问题

    能帮助开发者找出程序中读取未初始化内存的地方。

关于-fno-omit-frame-pointer参数:

-fno-omit-frame-pointer 是 GCC 编译器的一个选项,用于禁用帧指针优化,加上该参数,可以提升 AddressSanitizer 等内存检测工具的准确性,在初步了解阶段,本篇实例代码先不加此参数

3 实例

3.1 内存泄漏(memory leaks)

ASan对应内存泄漏的打印为:LeakSanitizer: detected memory leaks

写一个测试代码,通过malloc或new进行申请内存但未释放,导致内存泄漏:

//g++ -fsanitize=address -g test1.cpp -o test
#include <stdio.h>
#include <stdlib.h>int main()
{printf("hello\n");int *p1 = (int *) malloc(sizeof(int) * 5);int *p2 = new int(10);return 0;
}

编译运行后结果如下:

可以看到ASan工具检测到了内存泄漏:

  • 检测到20 byte的内存泄漏,在test1.cpp的第9行(int类型是4byte,x5=20byte,p1只申请了内存,未赋值)
  • 检测到4 byte的内存泄漏,在test1.cpp第11行(int类型是4byte,,p2申请的内存赋值为数值10)
  • 总结:在2处地方一共检测出24字节的内存泄漏

使用-g参数是为了将具体的代码行号能打印出来,如果不带-g参数,效果如下:

3.2 堆缓冲区溢出(heap-buffer-overflow)

堆缓冲区溢出,是指程序试图向堆内存的非法区域写入数据,导致越界访问

ASan对应堆缓冲区溢出的打印为:AddressSanitizer: heap-buffer-overflow

写一个测试代码,通过malloc申请内存(堆缓冲区),然后memcpy的数据长度超过malloc申请的长度,导致堆缓冲区溢出:

//g++ -fsanitize=address -g test1.cpp -o test
#include <stdio.h>
#include <stdlib.h>
#include <string.h>void test_heap_buffer_overflow()
{printf("[%s] in\n", __func__);char *p1 = (char *) malloc(sizeof(char) * 5);memcpy(p1, "hello world", sizeof("hello world"));free(p1);printf("[%s] out\n", __func__);
}int main()
{printf("[%s] in\n", __func__);test_heap_buffer_overflow();printf("[%s] out\n", __func__);return 0;
}

运行结果如下:

主要看这里:

WRITE of size 12 at 0x602000000015 thread T0#0 0x7f5470e6975c  (/usr/lib/x86_64-linux-gnu/libasan.so.5+0x3f75c)#1 0x5600b4f1bb7a in test_heap_buffer_overflow() /home/xxpcb/myTest/linux/ASan/HeapBuffOverflow/test1.cpp:11#2 0x5600b4f1bbc2 in main /home/xxpcb/myTest/linux/ASan/HeapBuffOverflow/test1.cpp:21#3 0x7f5470a5ac86 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21c86)#4 0x5600b4f1ba79 in _start (/home/xxpcb/myTest/linux/ASan/HeapBuffOverflow/test+0xa79)0x602000000015 is located 0 bytes to the right of 5-byte region [0x602000000010,0x602000000015)

错误位置

  • 写入地址:0x602000000015(堆内存)
  • 写入大小:12字节
  • 出错代码:/home/xxpcb/myTest/linux/ASan/HeapBuffOverflow/test1.cpp:11

内存分配信息

  • 出错地址位于一个5 字节内存块的右边界之外(0x6020000000100x602000000015,不包含结束地址)
  • 该内存块通过malloc()分配于:test1.cpp:10

3.3 栈缓冲区溢出(stack-buffer-overflow)

栈缓冲区溢出,是指程序试图向堆内存的非法区域写入数据,导致越界访问

ASan对应堆缓冲区溢出的打印为:AddressSanitizer: stack-buffer-overflow

写一个测试代码,通过声明一个固定长度的buf(栈缓冲区),然后memcpy的数据长度超过其固有长度,导致栈缓冲区溢出:

//g++ -fsanitize=address -g test1.cpp -o test
#include <stdio.h>
#include <stdlib.h>
#include <string.h>void test_stack_buffer_overflow()
{printf("[%s] in\n", __func__);char buf[5] = {0};memcpy(buf, "hello world", sizeof("hello world"));printf("[%s] out\n", __func__);
}int main()
{printf("[%s] in\n", __func__);test_stack_buffer_overflow();printf("[%s] out\n", __func__);return 0;
}

运行结果如下:

主要看这里:

=================================================================
==7451==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffee5037555 at pc 0x7f0b3f96975d bp 0x7ffee5037520 sp 0x7ffee5036cc8
WRITE of size 12 at 0x7ffee5037555 thread T0#0 0x7f0b3f96975c  (/usr/lib/x86_64-linux-gnu/libasan.so.5+0x3f75c)#1 0x55abda7d9d48 in test_stack_buffer_overflow() /home/xxpcb/myTest/linux/ASan/StackBufferOverflow/test1.cpp:11#2 0x55abda7d9dcf in main /home/xxpcb/myTest/linux/ASan/StackBufferOverflow/test1.cpp:20#3 0x7f0b3f55ac86 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21c86)#4 0x55abda7d9b69 in _start (/home/xxpcb/myTest/linux/ASan/StackBufferOverflow/test+0xb69)Address 0x7ffee5037555 is located in stack of thread T0 at offset 37 in frame#0 0x55abda7d9c34 in test_stack_buffer_overflow() /home/xxpcb/myTest/linux/ASan/StackBufferOverflow/test1.cpp:7This frame has 1 object(s):[32, 37) 'buf' <== Memory access at offset 37 overflows this variable

错误类型
stack-buffer-overflow(栈缓冲区溢出)

错误位置

  • 写入地址:0x7ffee5037555(栈内存)
  • 写入大小:12字节
  • 出错代码:/home/xxpcb/myTest/linux/ASan/StackBufferOverflow/test1.cpp:11

栈帧信息

  • 溢出发生在变量buf的偏移量37
  • buf的有效范围:[32, 37)(即 5 字节空间)
  • 变量声明于:test1.cpp:7

3.4 双重释放(double-Free)

双重释放,是指对已释放的内存块再次调用free,属于未定义行为

ASan对应双重释放的打印为:AddressSanitizer: attempting double-free

对同一指针调用free两次会导致严重的内存错误,属于未定义行为,可能引发以下后果:

  • 程序崩溃:第二次释放时,内存分配器可能发现该内存块已被释放,触发断言失败或崩溃(如 glibc 的malloc会触发double free or corruption错误)。
  • 内存泄漏与数据损坏:两次释放可能破坏内存分配器的元数据(如空闲链表结构),导致后续内存分配出现异常,甚至覆盖其他数据。

注意,free两次与free空指针是不同的,调用free(NULL)是合法操作,因为free函数在内部会先检查指针是否为NULL,若为NULL则直接返回,不执行释放内存的操作

另外,free后,指针是不为空,可以在下面测试代码中加打印验证。

测试代码:

//g++ -fsanitize=address -g test1.cpp -o test
#include <stdio.h>
#include <stdlib.h>
#include <string.h>void test_free_null()
{char *p1 = (char *) malloc(sizeof(char) * 64);memcpy(p1, "hello world", sizeof("hello world"));free(p1);if (nullptr == p1){printf("[%s] now p1 is nullptr\n", __func__);}free(p1);
}int main()
{test_free_null();return 0;
}

运行结果如下:

错误类型
double-free(双重释放)

内存地址与操作

  • 重复释放的地址:0x606000000020
  • 首次释放位置:test1.cpp:10
  • 第二次释放位置:test1.cpp:15
  • 内存块大小:64 字节(分配于test1.cpp:8

3.5 非法释放(free on address which was not malloc)

非法释放,释放的指针不是内存分配函数(malloc/calloc/realloc)返回的原始地址,分配器无法找到对应的元数据,导致校验失败。

ASan对应非法释放的打印为:AddressSanitizer: attempting free on address which was not malloc()-ed

测试代码

//g++ -fsanitize=address -g test2.cpp -o test
#include <stdio.h>
#include <stdlib.h>
#include <string.h>void test_free_null()
{char *p1 = (char *) malloc(sizeof(char) * 64);memcpy(p1, "hello world", sizeof("hello world"));char *p2 = p1 + 5;free(p2);free(p1);
}int main()
{test_free_null();return 0;
}

运行结果如下:

错误类型
bad-free(非法释放)

地址与操作细节

  • 尝试释放的地址:0x606000000025
  • 该地址位于一个 64 字节内存块的偏移 5 字节处(块范围:0x606000000020 ~ 0x606000000060
  • 内存块分配于:test2.cpp:8(使用malloc
  • 释放操作位于:test2.cpp:11

3.6 使用栈上返回的变量(stack-use-after-return)

当函数返回后,栈帧被销毁,内存变为无效,使用栈上返回的变量,例如打印返回的字符串hello world,可能有下面这些情况:

  • 垃圾值(如hello world后面跟随乱码)
  • 空值(导致段错误)
  • 仍然显示hello world(取决于栈内存是否被覆盖)

ASan对应的打印为:**AddressSanitizer: stack-use-after-return **

测试代码

//g++ -fsanitize=address -g test1.cpp -o test
//ASAN_OPTIONS=detect_stack_use_after_return=1 ./test
#include <stdio.h>
#include <stdlib.h>
#include <string.h>char *test_return_stack_p()
{char buf[64] = {0};memcpy(buf, "hello world", sizeof("hello world"));char *ret = &buf[0];printf("[%s] str:%s\n", __func__, ret);return ret;
}int main()
{char *str = test_return_stack_p();printf("[%s] str:%s\n", __func__, str);return 0;
}

运行结果如下:

直接运行是检测不到问题的,需要再加上ASAN_OPTIONS=detect_stack_use_after_return=1参数,加上参数的运行结果如下:

错误类型
stack-use-after-return(栈内存使用后返回)

关键位置

  • 无效读取发生在:test1.cpp:19main函数中的printf
  • 栈变量声明在:test1.cpp:7char buf[64]
  • 内存地址0x7f8fe8f00020属于buf数组的起始位置。

栈帧状态

  • ASan 标记栈帧为f5Stack after return),表示函数已返回,栈内存不再有效。
  • 读取操作(READ of size 12)访问了已释放的栈区域,触发错误。

3.7 使用退出作用域的变量(stack-use-after-scope)

ASan对应的打印为:AddressSanitizer: stack-use-after-scope

stack-use-after-scope与上面的stack-use-after-return比较类似,这里对比看下:

类型stack-use-after-returnstack-use-after-scope
触发时机函数返回后访问其栈帧内的变量变量作用域结束后访问该变量
内存状态函数栈帧已被销毁(通常被系统回收或覆盖)变量作用域结束,但栈帧可能尚未完全销毁
典型场景返回栈上变量的指针并在函数外使用在代码块结束后使用块内定义的变量
ASan 错误信息特征包含 stack-use-after-return 关键词包含 stack-use-after-scope 关键词
本质风险访问已被释放的栈内存(可能已被覆盖)访问作用域已结束的变量(值可能无效)

测试代码:

//g++ -fsanitize=address -g test1.cpp -o test
#include <stdio.h>
#include <stdlib.h>
#include <string.h>void test_stack_use_after_scope()
{char *str;{char buf[64] = {0};memcpy(buf, "hello world", sizeof("hello world"));str = &buf[0];printf("[%s] str:%s\n", __func__, str);}printf("[%s] str:%s\n", __func__, str);
}int main()
{test_stack_use_after_scope();return 0;
}

运行结果如下:

错误类型
stack-use-after-scope(栈作用域后使用)

关键位置

  • 无效读取发生在:test2.cpp:16test_stack_use_after_scope函数内)
  • 变量声明在:test2.cpp:7char buf[64]
  • 内存地址0x7ffd6e426590属于buf数组的起始位置。

栈内存状态

  • ASan 标记该内存区域为f8Stack use after scope),表示变量作用域已结束,内存逻辑上不再有效。
  • 读取操作(READ of size 12)触发了 ASan 的安全检查。

3.8 使用释放后的堆内存(heap-use-after-free)

ASan对应的打印为:AddressSanitizer: heap-use-after-free

测试代码:

//g++ -fsanitize=address -g test1.cpp -o test
#include <stdio.h>
#include <stdlib.h>
#include <string.h>void test_heap_use_after_free()
{char *p1 = (char *) malloc(sizeof(char) * 64);memcpy(p1, "hello world", sizeof("hello world"));char a = p1[0];printf("[%s] a:%c\n", __func__, a);free(p1);char b = p1[0];printf("[%s] b:%c\n", __func__, b);
}int main()
{test_heap_use_after_free();return 0;
}

运行结果:

错误类型
heap-use-after-free(堆内存使用后释放)

关键位置

  • 无效读取发生在:test1.cpp:14test_heap_use_after_free函数内)
  • 内存释放发生在:test1.cpp:12
  • 内存分配发生在:test1.cpp:8
  • 内存地址0x606000000020属于 64 字节堆块的起始位置。

内存状态

  • ASan 标记该内存区域为fdFreed heap region),表示内存已释放,不可访问。
  • 读取操作(READ of size 1)触发了 ASan 的安全检查,中断程序执行。

4 ASan报告解读归纳

上面的实例,演示了ASan检测出的多种内存问题,并生成了报告,对于这种报告的解读,再来归纳一下。

4.1 主要信息部分

  1. 找到 ERROR 开头的行:确认错误类型(如 detected memory leaksheap-buffer-overflowstack-use-after-scope)。
  2. 查看 READ/WRITE of size X:明确是读还是写越界,以及访问的字节数。
  3. 检查调用栈(#0 #1 ...:定位到代码中触发错误的具体行(文件名 + 行号)。

4.2 影子内存信息部分

ASan用影子内存(Shadow Memory) 标记程序内存的状态,每个影子字节对应 8 个应用程序字节,用于快速检测非法内存访问。

  • 地址范围0x1000303476800x0c047fff8050 是错误地址附近的影子内存区域。
  • 颜色与标记
影子字节含义作用 / 检测场景典型示例
00可访问内存(Addressable)标记正常可读写的内存区域全局变量、栈变量、合法堆内存(malloc/new 分配)
fa堆左边界红区(Heap left redzone)检测堆缓冲区溢出(malloc 前的保护)char* p = new char[10];p 前的 fa 区域
fd已释放堆内存(Freed heap region)检测 use-after-freefree(p); 后,p 指向的内存被标记为 fd
f1栈左边界红区(Stack left redzone)检测栈缓冲区溢出(函数栈帧前保护)函数局部数组 char buf[10]; 前的 f1 区域
f2栈中间红区(Stack mid redzone)检测栈变量越界(复杂栈布局保护)编译器插入的栈变量间保护区域
f3栈右边界红区(Stack right redzone)检测栈缓冲区溢出(函数栈帧后保护)函数局部数组 char buf[10]; 后的 f3 区域
f5函数返回后栈内存(Stack after return)检测 use-after-return函数返回后,栈帧被标记为 f5
f8作用域结束后栈内存(Stack use after scope)检测 use-after-scope代码块(if/for)内变量作用域结束后标记为 f8
f9全局红区(Global redzone)检测全局变量溢出全局数组 char g_buf[10]; 前后的 f9 区域
f6全局初始化顺序(Global init order)检测全局变量初始化依赖问题全局变量初始化时的特殊标记(ASan 内部使用)
f7用户毒化内存(Poisoned by user)检测手动毒化内存的访问__asan_poison_memory_region 标记的区域
fc容器溢出(Container overflow)检测 STL 容器越界(C++)vector<int> v(5); v[5] = 0; → 触发 fc
ac数组 Cookie(Array cookie)检测数组越界(堆 / 栈数组保护)堆数组 new char[10]; 前后的 ac 标记
bb对象内部红区(Intra object redzone)检测对象成员越界类成员变量间的保护区域(ASan 内部使用)
feASan 内部(ASan internal)ASan 自身使用的内存标记无需关注,仅用于工具内部
ca左变长数组红区(Left alloca redzone)检测变长数组(alloca)溢出alloca(10); 前的保护区域
cb右变长数组红区(Right alloca redzone)检测变长数组(alloca)溢出alloca(10); 后的保护区域

5 总结

本篇介绍了内存错误检测工具ASan的基础使用,并通过一些C/C++实例,来演示多种内存使用错误的场景,并分析ASan对应生成的错误信息报告,从而实现对程序中内存问题的定位。

http://www.lryc.cn/news/617299.html

相关文章:

  • 嵌入式Linnux学习 -- 软件编程2
  • uart通信中出现乱码,可能的原因是什么 ?
  • 借助 ChatGPT 快速实现 TinyMCE 段落间距与行间距调节
  • Nmap 渗透测试弹药库:精准扫描与隐蔽渗透技术手册
  • 什么是结构化思维?什么是结构化编程?
  • 计算机网络(一)——TCP
  • Vue脚手架模式与环境变量
  • 变频器实习DAY26 CDN 测试中心使用方法
  • Android16新特性速记
  • C语言如何安全的进行字符串拷贝
  • 从 GPT-2 到 gpt-oss:架构进步分析
  • 北京JAVA基础面试30天打卡07
  • Nacos-1--什么是Nacos?
  • 5G NR 非地面网络 (NTN)
  • JVM运维
  • C#(vs2015)利用unity实现弯管机仿真
  • 5G 非地面网络(NTN)最专业的方案
  • CSS accent-color:一键定制表单元素的主题色,告别样式冗余
  • 第2节 大模型分布式推理架构设计原则
  • XX生产线MES系统具体实施方案
  • 【Node.js从 0 到 1:入门实战与项目驱动】1.4 Node.js 的发展与生态(历史版本、LTS 版本、npm 生态系统)
  • CobaltStrike的搭建与使用
  • java基础概念(二)----变量(附练习题)
  • 【代码随想录day 17】 力扣 617.合并二叉树
  • 零外围双Buck 2C和2C1A!功率分配So Easy
  • Jmeter使用第二节-接口测试(Mac版)
  • MyBatis执行器与ORM特性深度解析
  • n8n中调用playwright-mcp 项目
  • ansible学习第一天
  • 定义短的魔术数字时小心负数的整型提升