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

本篇就来介绍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 leaks

Direct 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:7

  This 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-return stack-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-free free(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 内部使用)
fe ASan 内部(ASan internal) ASan 自身使用的内存标记 无需关注,仅用于工具内部
ca 左变长数组红区(Left alloca redzone) 检测变长数组(alloca)溢出 alloca(10); 前的保护区域
cb 右变长数组红区(Right alloca redzone) 检测变长数组(alloca)溢出 alloca(10); 后的保护区域

5 总结

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

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐