c/c++开发之内存错误检查工具asan、lsan、msan的介绍

Asan(Address Snitizer)简介

asan是一个由 LLVM 和 GCC 提供的内存错误检测工具,用于在程序运行时捕捉诸如堆/栈溢出、Use-after-free、内存泄漏、越界访问等常见的内存错误。它通过在编译阶段插入检查代码,并在运行时使用“影子内存”机制来监控内存访问,从而提供准确的错误检测和详细的报错信息,广泛应用于 C/C++ 程序的调试与安全检测中。

Asan实现原理

它通过在编译阶段插入检查代码,并在运行时使用“影子内存”机制来监控内存访问。

编译阶段插入检查代码:

当你使用 ASan 编译 C 或 C++ 程序时,编译器会把一些额外的检查代码嵌入到你的程序中。这些检查代码并不是你自己写的,而是编译器自动生成的。

  • 这些代码的作用是监控程序在运行时对内存的访问,比如访问数组边界、检查是否在使用已经释放的内存等。

  • 它们会“拦截”内存操作,确保程序不会访问到不该访问的内存地址。

运行时使用“影子内存”机制

影子内存是一个非常重要的概念,它是 ASan 在程序运行时用来跟踪内存状态的工具。

  • 简单来说,影子内存就是一个用于监控正常内存的额外内存区域,它的作用是记录每一块内存的状态(比如是否已分配、是否已释放、是否已越界等)。

  • 每次程序访问内存时,ASan 会通过“影子内存”来检查这块内存是否可以正常访问。

举个例子:

  • 假设你有一个大小为 100 字节的数组,程序会使用 100 字节的“影子内存”来对应它。

  • 如果你访问数组的第 101 个元素,ASan 会发现这个访问超出了实际内存的范围,进而报告内存越界错误。

  • 影子内存记录着内存块的状态,帮助 ASan 精确地定位出内存错误。

Asan主要特性

1. 越界访问检测

  • 堆越界访问(Heap buffer overflow)

    • 检测访问 new/malloc 分配的内存块之外的区域。

  • 栈越界访问(Stack buffer overflow)

    • 检测访问函数局部变量数组越界的行为。

  • 全局变量越界访问

    • 检测静态全局数组变量的越界使用(依赖编译器支持)。

2. 使用后释放检测(Use-after-free)

  • 检测访问已经释放的堆内存(如 deletefree 之后的访问)。

  • ASan 会将释放后的内存标记为“中毒(poisoned)”,并触发错误报告。

3. 内存泄漏检测(Memory Leak Detection)

  • GCC 不支持内存泄漏检测(需配合 LeakSanitizer)

  • Clang 默认集成了 LeakSanitizer,可以检测未释放的堆内存。需要在运行程序时加上 ASAN_OPTIONS=detect_leaks=1

4. 堆栈内存错误检测(Stack Use-after-scope)

  • 检测对已经离开作用域的栈变量的访问。

  • 类似 use-after-free,但适用于栈变量。

Asan的基本使用(以gcc/g++下的Asan为例)

1.要求gcc/g++版本大于4.8,确保安装了libasan

ls /usr/lib | grep libasan

2.编译程序:启用-fsanitize=address

g++ -fsanitize=address -g -o example example.cpp

        -fsanitize=address:启用asan

        -g:包含调试信息,以便asan提供详细的错误报告

        -o:指定输出的可执行文件名称

3.运行编译后的可执行文件

./example

运行时,asan会监听内存访问并在检测到错误时输出报告

4.错误报告解析

示例:

example.cpp

#include <iostream>

int main() {
    int* ptr = new int(42);  // 分配堆内存
    delete ptr;              // 提前释放内存
    std::cout << *ptr << std::endl;  // 错误:使用已释放内存
    return 0;
}

asan检测结果:

==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010 at pc 0x00000040085d bp 0x7ffd68f0f690 sp 0x7ffd68f0f688
READ of size 4 at 0x602000000010 thread T0
    #0 0x40085c in main /path/to/example.cpp:7
    #1 0x7f2c6eeb9082 in __libc_start_main (/lib64/libc.so.6+0x24082)
    #2 0x4006ad in _start (/path/to/example+0x4006ad)

0x602000000010 is located 0 bytes inside of 4-byte region [0x602000000010,0x602000000014)
freed by thread T0 here:
    #0 0x7f2c6f39c65f in operator delete(void*) (/usr/lib/libasan.so.6+0xad65f)
    #1 0x40082d in main /path/to/example.cpp:6

previously allocated by thread T0 here:
    #0 0x7f2c6f39b93f in operator new(unsigned long) (/usr/lib/libasan.so.6+0xac93f)
    #1 0x4007fd in main /path/to/example.cpp:5

报错信息解析:

部分位置内容示例
==12345==ERROR: AddressSanitizer: heap-use-after-free指明发生了 堆上释放后使用 的错误。
on address 0x602000000010访问出错的内存地址。
at pc 0x00000040085d bp 0x7ffd68f0f690程序计数器和栈信息,用于调试定位。
READ of size 4表示试图读取 4 个字节(int)的数据。
#0 0x40085c in main /path/to/example.cpp:7指出错误发生在 example.cpp 第 7 行:std::cout << *ptr
freed by thread T0 here:表示这块内存是在哪里被释放的。
#1 0x40082d in main /path/to/example.cpp:6删除操作发生在第 6 行:delete ptr
previously allocated by thread T0 here:指出这块内存原先是在哪里分配的。
#1 0x4007fd in main /path/to/example.cpp:5分配操作发生在第 5 行:new int(42)

 内存相关错误示例的asan验证

1.栈缓冲区溢出

栈缓冲区溢出(Stack Buffer Overflow)是指:程序向栈上分配的数组或缓冲区中写入了超出其边界的数据,从而破坏了临近的栈内存内容,可能导致程序崩溃、行为异常,甚至被恶意利用来执行任意代码(如传统的栈溢出攻击)

示例:

int main() {
    char buffer[10];  // 栈上分配一个长度为10的字符数组
    std::strcpy(buffer, "This string is too long!");  // 溢出:写入超出10字节
    std::cout << buffer << std::endl;
    return 0;
}

asan检测结果:stack_buffer_orerflow

=================================================================
==12345==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fffdc6e9b10 at pc 0x000000401234 bp 0x7fffdc6e9a70 sp 0x7fffdc6e9a68
WRITE of size 25 at 0x7fffdc6e9b10 thread T0
    #0 0x401234 in main /path/to/stack_overflow.cpp:7
    #1 0x7f3a2dcf5082 in __libc_start_main (/lib64/libc.so.6+0x24082)
    #2 0x4010ad in _start (/path/to/stack_overflow+0x4010ad)

Address 0x7fffdc6e9b10 is located in stack of thread T0 at offset 16 in frame
    #0 0x401200 in main /path/to/stack_overflow.cpp:5

  This frame has 1 object(s):
    [16, 26) 'buffer' <== Memory access at 0x7fffdc6e9b10 is out of bounds

 2.堆缓冲区溢出

示例:

int main() {
    int* arr = new int[5];  // 分配5个int的堆数组
    for (int i = 0; i <= 5; ++i) {  //越界访问,第6次写入非法
        arr[i] = i * 10;
    }
    delete[] arr;
    return 0;
}

asan检测结果:heap-buffer-overflow

=================================================================
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000014 at pc 0x000000400abc bp 0x7ffc12345678 sp 0x7ffc12345670
WRITE of size 4 at 0x602000000014 thread T0
    #0 0x400abc in main /path/to/heap_overflow.cpp:6
    #1 0x7f3b4a0c5082 in __libc_start_main (/lib64/libc.so.6+0x24082)
    #2 0x40070d in _start (/path/to/heap_overflow+0x40070d)

0x602000000014 is located 4 bytes to the right of 20-byte region [0x602000000000,0x602000000014)
allocated by thread T0 here:
    #0 0x7f3b4a41c018 in operator new[](unsigned long) (libasan.so) +0x98
    #1 0x400a6a in main /path/to/heap_overflow.cpp:4

3.全局缓冲区溢出 

全局缓冲区溢出(Global Buffer Overflow)是指对定义在全局或静态作用域的数组或缓冲区进行越界访问,无论是读操作还是写操作。

示例:

char buffer[10];  // 全局缓冲区
int main() {
    for (int i = 0; i <= 10; ++i) {  // 越界写 buffer[10]
        buffer[i] = 'A' + i;
    }
    return 0;
}

asan检测结果:global-buffer-overflow

=================================================================
==12345==ERROR: AddressSanitizer: global-buffer-overflow on address 0x602000000010 at pc 0x000000400aab bp 0x7ffd9a1db7a0 sp 0x7ffd9a1db790
WRITE of size 1 at 0x602000000010 thread T0
    #0 0x400aab in main /path/to/global_buffer_overflow.cpp:6
    #1 0x7f1e98c45082 in __libc_start_main (/lib64/libc.so.6+0x24082)
    #2 0x40070d in _start (/path/to/global_buffer_overflow+0x40070d)

0x602000000010 is located 1 bytes to the right of 10-byte region [0x602000000000,0x60200000000a)
allocated by thread T0 here:
    #0 0x7f1e98bdb018 in operator new[](unsigned long) (libasan.so) +0x98
    #1 0x4009b5 in main /path/to/global_buffer_overflow.cpp:4

4.使用已释放的内存

示例:

int main() {
    int* ptr = new int(42);   // 在堆上分配一个整数并赋值为 42
    delete ptr;               // 释放堆内存
    std::cout << *ptr << std::endl;  // 再次访问已释放的内存(未定义行为)
    return 0;
}

asan检测结果:heap-use-after-free

=================================================================
==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010 at pc 0x000000400abc bp 0x7ffc12345678 sp 0x7ffc12345670
READ of size 4 at 0x602000000010 thread T0
    #0 0x400abc in main /path/to/use_after_free.cpp:7
    #1 0x7f4d3e27b082 in __libc_start_main (/lib64/libc.so.6+0x24082)
    #2 0x40070d in _start (/path/to/use_after_free+0x40070d)

0x602000000010 is located 0 bytes inside of 4-byte region [0x602000000010,0x602000000014)
freed by thread T0 here:
    #0 0x7f4d3e5cd818 in operator delete(void*) (libasan.so) +0x98
    #1 0x400aab in main /path/to/use_after_free.cpp:6

previously allocated by thread T0 here:
    #0 0x7f4d3e5cd018 in operator new(unsigned long) (libasan.so) +0x98
    #1 0x400a6a in main /path/to/use_after_free.cpp:5

5.双重释放

示例:

int main() {
    int* ptr = new int(42);  // 分配内存
    delete ptr;              // 第一次释放
    delete ptr;              // 第二次释放(非法)
    return 0;
}

asan检测结果:double-free

=================================================================
==12345==ERROR: AddressSanitizer: attempting double-free on 0x602000000010:
    #0 0x7f3c9b6fc018 in operator delete(void*) (libasan.so) +0x98
    #1 0x400abc in main /path/to/double_free.cpp:7
    #2 0x7f3c9a3c2082 in __libc_start_main (/lib64/libc.so.6+0x24082)
    #3 0x40070d in _start (/path/to/double_free+0x40070d)

0x602000000010 is located 0 bytes inside of 4-byte region [0x602000000010,0x602000000014)
freed by thread T0 here:
    #0 0x7f3c9b6fc018 in operator delete(void*) (libasan.so) +0x98
    #1 0x400a9f in main /path/to/double_free.cpp:6

SUMMARY: AddressSanitizer: double-free /path/to/double_free.cpp:7 in main

6.悬挂指针非法访问

悬挂指针指的是指向已经释放的内存区域的指针。这种指针依然保留着旧的内存地址,但该地址的内容已经不再有效。如果程序在访问这些已经释放的内存时,就会出现悬挂指针非法访问的问题。悬挂指针可能会导致未定义的行为,包括程序崩溃、数据损坏、甚至安全漏洞。

示例:

void testDanglingPointer() {
    int* ptr = new int(10);  // 动态分配内存
    delete ptr;               // 释放内存
    std::cout << *ptr << std::endl;  // 访问已释放的内存(悬挂指针)
}

int main() {
    testDanglingPointer();
    return 0;
}

asan检测结果:heap-use-after-free

=================================================================
==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010 at pc 0x0000004009b2 bp 0x7ffc7c2db7a0 sp 0x7ffc7c2db790
READ of size 4 at 0x602000000010 thread T0
    #0 0x4009b2 in testDanglingPointer /path/to/dangling_pointer.cpp:7
    #1 0x4009e0 in main /path/to/dangling_pointer.cpp:11
    #2 0x7f1e98c45082 in __libc_start_main (/lib64/libc.so.6+0x24082)
    #3 0x40070d in _start (/path/to/dangling_pointer+0x40070d)

0x602000000010 is located 0 bytes inside of 4-byte region [0x602000000010,0x602000000014)
freed by thread T0 here:
    #0 0x7f1e98bdf018 in operator delete(void*) (libasan.so) +0x98
    #1 0x4009a9 in testDanglingPointer /path/to/dangling_pointer.cpp:5

SUMMARY: AddressSanitizer: heap-use-after-free /path/to/dangling_pointer.cpp:7 in testDanglingPointer

7.函数返回后,仍然尝试访问函数的栈上局部变量

局部变量通常存储在栈上,当函数调用结束后,栈上的内存空间会被释放覆盖,所以访问这些已释放的内存区域会导致未定义行为。

示例:

int* stackUseAfterReturn() {
    int localVar = 42;  // 栈上局部变量
    return &localVar;    // 返回局部变量的地址
}

int main() {
    int* ptr = stackUseAfterReturn();
    std::cout << *ptr << std::endl;  // 函数返回后访问局部变量
    return 0;
}

asan检测结果:stack-use-after-return

=================================================================
==12345==ERROR: AddressSanitizer: stack-use-after-return on address 0x7fffd2454c28 at pc 0x0000004007a2 bp 0x7fffd2454c60 sp 0x7fffd2454bf0
READ of size 4 at 0x7fffd2454c28 thread T0
    #0 0x4007a2 in main /path/to/stack_use_after_return.cpp:9
    #1 0x7f1e98c45082 in __libc_start_main (/lib64/libc.so.6+0x24082)
    #2 0x40070d in _start (/path/to/stack_use_after_return+0x40070d)

0x7fffd2454c28 is located 8 bytes to the right of 4-byte region [0x7fffd2454c20,0x7fffd2454c24)
freed by thread T0 here:
    #0 0x7f1e98bdf018 in operator delete(void*) (libasan.so) +0x98
    #1 0x4007a9 in stackUseAfterReturn /path/to/stack_use_after_return.cpp:6

SUMMARY: AddressSanitizer: stack-use-after-return /path/to/stack_use_after_return.cpp:9 in main

8.作用域后使用

示例:

void useAfterScope() {
    {
        int localVar = 42;  // 在一个小作用域内定义局部变量
        std::cout << "In inner scope, localVar: " << localVar << std::endl;
    }  // 小作用域结束,localVar 超出作用域

    // 尝试在小作用域外访问 localVar,已经超出作用域
    std::cout << "After inner scope, localVar: " << localVar << std::endl;  //错误:访问超出作用域的局部变量
}

int main() {
    useAfterScope();
    return 0;
}

asan检测结果:stack-use-after-scope

=================================================================
==12345==ERROR: AddressSanitizer: stack-use-after-scope on address 0x7ffd97f56c20 at pc 0x0000004007c1 bp 0x7ffd97f56c50 sp 0x7ffd97f56c10
READ of size 4 at 0x7ffd97f56c20 thread T0
    #0 0x4007c1 in useAfterScope /path/to/stack_use_after_scope.cpp:13
    #1 0x7f1e98c45082 in __libc_start_main (/lib64/libc.so.6+0x24082)
    #2 0x40070d in _start (/path/to/stack_use_after_scope+0x40070d)

0x7ffd97f56c20 is located 0 bytes inside of 4-byte region [0x7ffd97f56c20,0x7ffd97f56c24)
freed by thread T0 here:
    #0 0x7f1e98bdf018 in operator delete(void*) (libasan.so) +0x98
    #1 0x4007c9 in useAfterScope /path/to/stack_use_after_scope.cpp:6

SUMMARY: AddressSanitizer: stack-use-after-scope /path/to/stack_use_after_scope.cpp:13 in useAfterScope

Asan能够检测出的问题汇总

错误类型简要说明
stack-buffer-overflow栈缓冲区溢出。程序试图读取或写入栈上分配的缓冲区的边界之外的内存,通常是数组越界。
heap-buffer-overflow堆缓冲区溢出。程序试图读取或写入堆上分配的缓冲区的边界之外的内存,可能导致数据泄露或崩溃。
global-buffer-overflow全局缓冲区溢出。程序试图读取或写入全局变量分配的缓冲区的边界之外的内存。
heap-use-after-free使用已释放的堆内存。程序在释放堆内存后继续访问这块内存,导致未定义行为。
double-free双重释放。程序尝试两次释放同一块内存,可能导致程序崩溃或安全漏洞。
stack-use-after-free栈内存使用后访问。程序在栈上的局部变量超出作用域后仍然试图访问该内存。
stack-use-after-return函数返回后访问栈内存。程序在函数返回后,仍然尝试访问该函数栈上已销毁的局部变量。
stack-use-after-scope作用域后使用。程序在局部变量超出作用域后继续访问该变量,导致未定义行为。

Lsan

Lsan简介

用于在程序运行时检测是否存在 内存泄漏(即,程序分配了内存但未释放,导致内存无法再使用)。

内存泄漏:asan检测的非法内存访问错误很快就会暴露出来,但内存泄漏有时即使长时间运行也不会暴露出来,这就导致内存泄漏是不易被发现的。内存泄漏可能并不严重,甚至无法通过正常方法检测出到。在现在操作系统中,应用程序使用的内存在应用程序终止时由操作系统统一释放,这意味着只运行很短时间的程序中的内存泄漏可能不会被注意到并且很少是严重的。

Lsan使用

与asan类似,并且lsan一般配合asan一起开启,一起使用;在编译时加上-fsanitize=leak

内存泄漏错误示例的lsan验证

示例:

void memory_leak_example() {
    int* ptr = (int*)malloc(sizeof(int) * 10);  // 分配了 10 个整数的内存
    // 但是没有释放这个内存,导致内存泄漏
}

int main() {
    memory_leak_example();  // 调用内存泄漏的函数
    return 0;
}

lsan检测结果:detected memory leaks

==12345==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 40 byte(s) in 1 object(s) allocated from:
    #0 0x... in malloc
    #1 0x... in memory_leak_example() memory_leak_example.cpp:6
    #2 0x... in main memory_leak_example.cpp:9

SUMMARY: LeakSanitizer: 40 byte(s) leaked in 1 allocation(s).

Msan

Msan简介

MSan 主要的功能是帮助开发者发现程序中对未初始化内存的访问,这种访问可能会导致不确定的行为、错误输出、崩溃等问题。

Msan使用

-fsanitize=memory

未初始化内存访问错误示例的msan验证

示例:

-void use_uninitialized_memory() {
    int x;  // 未初始化的局部变量
    std::cout << "Value of x: " << x << std::endl;  // 使用了未初始化的变量
}

int main() {
    use_uninitialized_memory();
    return 0;
}

msan检测结果:use-of-uninitialized-value

==12345==WARNING: MemorySanitizer: use of uninitialized value
    #0 0x... in use_uninitialized_memory() my_program.cpp:6
    #1 0x... in main my_program.cpp:10
    ...

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值