面向开发的内存调试神器,如何使用ASAN检测内存泄漏、堆栈溢出等问题

asan原理详解 Google ASan工具ASAN,全称 AddressSanitizer,也即地址消毒技术。可以用来检测内存问题,例如缓冲区溢出或对悬空指针的非法访问等。 阅读详情
AI摘要

本文详细介绍了AddressSanitizer(ASAN)工具,它是谷歌开源的内存错误检测工具,用于检测内存泄漏、悬空指针、堆溢出、栈溢出和全局缓冲区溢出等问题。ASAN通过插入代码插桩和使用shadow内存进行实时检测,相比Valgrind性能损耗更低。通过实例展示了ASAN如何检测和报告各种内存错误,以及如何在编译时启用ASAN。此外,还解释了ASAN的基本原理,包括shadow内存、redzone和代码插桩。

  • GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源。

[toc]

介绍

首先,先介绍一下 Sanitizer 项目,该项目是谷歌出品的一个开源项目,该项目包含了 ASAN、LSAN、MSAN、TSAN等内存、线程错误的检测工具,这里简单介绍一下这几个工具的作用:

  • ASAN: 内存错误检测工具,在编译命令中添加-fsanitize=address启用

  • LSAN: 内存泄漏检测工具,已经集成到 ASAN 中,可以通过设置环境变量ASAN_OPTIONS=detect_leaks=0来关闭ASAN上的LSAN,也可以使用-fsanitize=leak编译选项代替-fsanitize=address来关闭ASAN的内存错误检测,只开启内存泄漏检查。

  • MSAN: 对程序中未初始化内存读取的检测工具,可以在编译命令中添加-fsanitize=memory -fPIE -pie启用,还可以添加-fsanitize-memory-track-origins选项来追溯到创建内存的位置

  • TSAN: 对线程间数据竞争的检测工具,在编译命令中添加-fsanitize=thread启用 其中ASAN就是我们今天要介绍的重头戏。

ASAN,全称 AddressSanitizer,可以用来检测内存问题,例如缓冲区溢出或对悬空指针的非法访问等。

根据谷歌的工程师介绍 ASAN 已经在 chromium 项目上检测出了300多个潜在的未知bug,而且在使用 ASAN 作为内存错误检测工具对程序性能损耗也是及其可观的。

根据检测结果显示可能导致性能降低2倍左右,比Valgrind(官方给的数据大概是降低10-50倍)快了一个数量级。

而且相比于Valgrind只能检查到堆内存的越界访问和悬空指针的访问,ASAN 不仅可以检测到堆内存的越界和悬空指针的访问,还能检测到栈和全局对象的越界访问。

这也是 ASAN 在众多内存检测工具的比较上出类拔萃的重要原因,基本上现在 C/C++ 项目都会使用ASAN来保证产品质量,尤其是大项目中更为需要。

如何使用 ASAN

作为如此强大的神兵利器,自然是不会在程序员的战场上失宠的。

从LLVM3.1、GCC4.8、XCode7.0、MSVC16.9开始ASAN就已经成为众多主流编译器的内置工具了,因此,要在项目中使用ASAN也是十分方便。

现在只需要在编译命令中加上-fsanitize=address检测选项就可以让ASAN在你的项目中大展神通,接下来通过几个例子来看一下 ASAN 到底有哪些本领。

注意:

  1. 在下面的例子中打开了调试标志-g,这是因为当发现内存错误时调试符号可以帮助错误报告更准确的告知错误发生位置的堆栈信息,如果错误报告中的堆栈信息看起来不太正确,请尝试使用-fno-omit-frame-pointer来改善堆栈信息的生成情况。
  2. 如果构建代码时,编译和链接阶段分开执行,则必须在编译和链接阶段都添加-fsanitize=address选项。

检测内存泄漏

// leak.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, const char *argv[]) {
    char *s = (char*)malloc(100);
    strcpy(s, "Hello world!");
    printf("string is: %s\n", s);
    return 0;
}

上述代码中我们分配了100个字节的内存空间,但在main函数返回前始终没有释放,接下来我们使用ASAN看一下是否能够检测出来,添加-fsanitize=address -g参数构建代码并执行:

~/Code/test$ gcc noleak.c -o noleak -fsanitize=address -g
~/Code/test$ ./leak 
string is: Hello world!

=================================================================
==1621572==ERROR: LeakSanitizer: detected memory leaks    // 1)

Direct leak of 100 byte(s) in 1 object(s) allocated from:   // 2)
    #0 0x7f5b986bc808 in __interceptor_malloc ../../../../src/libsanitizer/ASAN/ASAN_malloc_linux.cc:144
    #1 0x562d866b5225 in main /home/chenbing/Code/test/leak.c:7
    #2 0x7f5b983e1082 in __libc_start_main ../csu/libc-start.c:308

SUMMARY: AddressSanitizer: 100 byte(s) leaked in 1 allocation(s).

这里,ASAN 提供的报告说明了错误原因是detected memory leaks内存泄漏了1),同时,2)说明ASAN检测到应用程序分配了100个字节,并捕获到了内存分配位置的堆栈信息,还告诉了我们内存是在leak.c:7分配的。

有了这么详细的且准确的错误报告,内存问题是不是不那么头疼了?

检测悬空指针访问

// uaf.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, const char *argv[]) {
    char *s = (char*)malloc(100);
    free(s);
    strcpy(s, "Hello world!");  // use-after-free
    printf("string is: %s\n", s);
    return 0;
}

上述代码中我们分配了100个字节的内存空间,紧接着将其释放,但接下来我们对之前分配的内存地址执行写入操作,这是典型的悬空指针非法访问,同样,让我们使用ASAN看一下是否能够检测出来,添加-fsanitize=address -g参数构建代码并执行:

~/Code/test$ gcc uaf.c -o uaf -fsanitize=address -g
~/Code/test$ ./uaf 
=================================================================
==1624341==ERROR: AddressSanitizer: heap-use-after-free on address 0x60b0000000f0 at pc 0x7f9f776bb58d bp 0x7fffabad8280 sp 0x7fffabad7a28    // 1)
WRITE of size 13 at 0x60b0000000f0 thread T0  // 2)
    #0 0x7f9f776bb58c in __interceptor_memcpy ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:790
    #1 0x55b9cf56e26d in main /home/chenbing/Code/test/uaf.c:9
    #2 0x7f9f77452082 in __libc_start_main ../csu/libc-start.c:308
    #3 0x55b9cf56e16d in _start (/home/chenbing/Code/test/uaf+0x116d)

0x60b0000000f0 is located 0 bytes inside of 100-byte region [0x60b0000000f0,0x60b000000154) // 3)
freed by thread T0 here:
    #0 0x7f9f7772d40f in __interceptor_free ../../../../src/libsanitizer/ASAN/ASAN_malloc_linux.cc:122
    #1 0x55b9cf56e255 in main /home/chenbing/Code/test/uaf.c:8
    #2 0x7f9f77452082 in __libc_start_main ../csu/libc-start.c:308

previously allocated by thread T0 here: // 4)
    #0 0x7f9f7772d808 in __interceptor_malloc ../../../../src/libsanitizer/ASAN/ASAN_malloc_linux.cc:144
    #1 0x55b9cf56e245 in main /home/chenbing/Code/test/uaf.c:7
    #2 0x7f9f77452082 in __libc_start_main ../csu/libc-start.c:308

SUMMARY: AddressSanitizer: heap-use-after-free ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:790 in __interceptor_memcpy
Shadow bytes around the buggy address:  // 5)
  0x0c167fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c167fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c167fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c167fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c167fff8000: fa fa fa fa fa fa fa fa fd fd fd fd fd fd fd fd
=>0x0c167fff8010: fd fd fd fd fd fa fa fa fa fa fa fa fa fa[fd]fd
  0x0c167fff8020: fd fd fd fd fd fd fd fd fd fd fd fa fa fa fa fa
  0x0c167fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c167fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c167fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c167fff8060: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASAN internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
  Shadow gap:              cc
==1624341==ABORTING

这个错误报告看起来很长,但实际上并不复杂,

  • 1)告诉我们错误的原因是:heap-use-after-free,访问了悬空指针,该内存的地址是:0x60b0000000f0,同时还告诉我们发生错误时的PC、BP、SP寄存器的内容,这些我们可以不关心,因为接下来的报告让我们可以忽略这些寄存器就可以定位到问题。

  • 接下来是2), 3), 4),分别报告了访问悬空指针的位置、内存被释放位置、内存的分配位置的堆栈信息以及线程信息,从2)可以看到错误发生在uaf.c文件的第8行代码。报告中的其他部分

  • 5)提供了错误访问的内存地址对应的shadow 内存的详细,其中fa表示堆区内存的red zone,fd表示已经释放的堆区内存。

检测堆溢出

// overflow.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, const char *argv[]) {
    char *s = (char*)malloc(12);
    strcpy(s, "Hello world!");
    printf("string is: %s\n", s);
    free(s);
    return 0;
}

上面这段代码我们只分配了2个字节,但在随后操作中写入了13字节的数据(字符串还包含\0做为终止符),此时,数据的写入显然是溢出分配的内存块了,同样,添加-fsanitize=address -g参数构建代码并执行:

~/Code/test$ gcc overflow.c -o overflow -fsanitize=address -g
~/Code/test$ ./overflow 
=================================================================
==2172878==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000001c at pc 0x7f1cd3d3d58d bp 0x7ffee78e6500 sp 0x7ffee78e5ca8     //1)
WRITE of size 13 at 0x60200000001c thread T0        // 2)
    #0 0x7f1cd3d3d58c in __interceptor_memcpy ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:790
    #1 0x555593131261 in main /home/chenbing/Code/test/overflow.c:7
    #2 0x7f1cd3ad4082 in __libc_start_main ../csu/libc-start.c:308
    #3 0x55559313116d in _start (/home/chenbing/Code/test/overflow+0x116d)

0x60200000001c is located 0 bytes to the right of 12-byte region [0x602000000010,0x60200000001c)    // 3)
allocated by thread T0 here:
    #0 0x7f1cd3daf808 in __interceptor_malloc ../../../../src/libsanitizer/ASAN/ASAN_malloc_linux.cc:144
    #1 0x555593131245 in main /home/chenbing/Code/test/overflow.c:6
    #2 0x7f1cd3ad4082 in __libc_start_main ../csu/libc-start.c:308

SUMMARY: AddressSanitizer: heap-buffer-overflow ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:790 in __interceptor_memcpy
Shadow bytes around the buggy address:      // 4)
  0x0c047fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c047fff8000: fa fa 00[04]fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASAN internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
  Shadow gap:              cc
==2172878==ABORTING

上面的报告访问悬空指针的错误报告很相似,同样

1)告诉我们错误的原因是:heap-buffer-overflow,堆区内存溢出了,该内存的地址是:0x60200000001c。

2)描述了写入数据导致溢出的位置堆栈,

3)则是对应的内存分配位置堆栈,4)还是shadow内存快照。

C++ 中的new/delete不匹配

// bad_delete.cpp
#include <iostream>
#include <cstring>

int main(int argc, const char *argv[]) {
    char *cstr = new char[100];
    strcpy(cstr, "Hello World");
    std::cout << cstr << std::endl;

    delete cstr;
    return 0;
}

这段代码通过new[]关键字分配了一块内存,但是在函数返回前却是使用delete堆内存进行释放,而不是delete[],这将导致分配的内存没有被完全释放,还是添加-fsanitize=address -g参数构建代码并执行:

~/Code/test$ g++ bad_delete.cpp -o bad_delete -fsanitize=address -g
~/Code/test$ ./bad_delete 
Hello World
=================================================================
==2180936==ERROR: AddressSanitizer: alloc-dealloc-mismatch (operator new [] vs operator delete) on 0x60b0000000f0     // 1
    #0 0x7fa9f877cc65 in operator delete(void*, unsigned long) ../../../../src/libsanitizer/ASAN/ASAN_new_delete.cc:177
    #1 0x55d09d3fe33f in main /home/chenbing/Code/test/bad_delete.cpp:10
    #2 0x7fa9f8152082 in __libc_start_main ../csu/libc-start.c:308
    #3 0x55d09d3fe20d in _start (/home/chenbing/Code/test/bad_delete+0x120d)

0x60b0000000f0 is located 0 bytes inside of 100-byte region [0x60b0000000f0,0x60b000000154)       // 2
allocated by thread T0 here:
    #0 0x7fa9f877b787 in operator new[](unsigned long) ../../../../src/libsanitizer/ASAN/ASAN_new_delete.cc:107
    #1 0x55d09d3fe2e5 in main /home/chenbing/Code/test/bad_delete.cpp:6
    #2 0x7fa9f8152082 in __libc_start_main ../csu/libc-start.c:308

SUMMARY: AddressSanitizer: alloc-dealloc-mismatch ../../../../src/libsanitizer/ASAN/ASAN_new_delete.cc:177 in operator delete(void*, unsigned long)
==2180936==HINT: if you don't care about these errors you may set ASAN_OPTIONS=alloc_dealloc_mismatch=0
==2180936==ABORTING

这份错误报告比上面两个要简要的多,但提供的信息已经完全足够定位问题了:

1)汇报了错误类型:alloc-dealloc-mismatch,分配和释放操作不匹配,该内存的地址是:0x60b0000000f0,

2)是对应的内存分配位置堆栈,该报告不会明确告诉错误的位置应该使用delete[]对内存进行释放,因为在C++中分配和释放关键字可以被重写或者其他特定场景不匹配的关键字也能完全释放内存。

因此,ASAN不能保证alloc-dealloc-mismatch一定符合用户的期望,所以,在该报告中ASAN说明了:如果这对用户来说这是一个误报的错误,那么可以使用ASAN_OPTIONS=alloc_dealloc_mismatch=0来禁用该报告的触发,

例如:

~/Code/test$ ASAN_OPTIONS=alloc_dealloc_mismatch=0 ./bad_delete 
Hello World

上面执行代码时添加了ASAN_OPTIONS=alloc_dealloc_mismatch=0参数,因此,ASAN不会认为alloc-dealloc-mismatch是一个错误,从而发出错误报告。

检测栈溢出

// sbo.c
#include <stdio.h>

int main(int argc, const char *argv[]) {
    int stack_array[100];
    stack_array[101] = 1;
    return 0;
}

上面的代码,我们在栈上创建了一个容量为100的数组,但在随后的写入操作中在超过数据容量的地址上写入数据,导致了栈溢出,添加-fsanitize=address -g参数构建代码并执行:

~/Code/test$ g++ sbo.c -o sbo -fsanitize=address -g
chenbing@GreatDB-CB:~/Code/test$ ./sbo 
=================================================================
==2196928==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffc33777f24 at pc 0x562dccb592b6 bp 0x7ffc33777d40 sp 0x7ffc33777d30    1)
WRITE of size 4 at 0x7ffc33777f24 thread T0
    #0 0x562dccb592b5 in main /home/chenbing/Code/test/sbo.c:6
    #1 0x7f45bf52d082 in __libc_start_main ../csu/libc-start.c:308
    #2 0x562dccb5910d in _start (/home/chenbing/Code/test/sbo+0x110d)

Address 0x7ffc33777f24 is located in stack of thread T0 at offset 452 in frame    2)
    #0 0x562dccb591d8 in main /home/chenbing/Code/test/sbo.c:4

  This frame has 1 object(s):     3)
    [48, 448) 'stack_array' (line 5) <== Memory access at offset 452 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork  4)
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow /home/chenbing/Code/test/sbo.c:6 in main
Shadow bytes around the buggy address:    5)
  0x1000066e6f90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x1000066e6fa0: 00 00 00 00 00 00 00 00 00 00 00 00 f1 f1 f1 f1
  0x1000066e6fb0: f1 f1 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x1000066e6fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x1000066e6fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x1000066e6fe0: 00 00 00 00[f3]f3 f3 f3 f3 f3 f3 f3 00 00 00 00
  0x1000066e6ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x1000066e7000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x1000066e7010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x1000066e7020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x1000066e7030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASAN internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
  Shadow gap:              cc
==2196928==ABORTING

这份报告的内容基本与上面几本报告的内容相似,这里不再做过多解释,我们来关注几个不同的地方,

3)说明了栈对象的在函数栈区的偏移范围是[48, 448)(左闭右开),而代码中通过栈对象访问的位置却是512导致了栈溢出。

还有一个地方需要在注意:报告中提到了一个可能错报的栈溢出场景:如果程序使用一些特殊的堆栈展开机制,swapcontext或者vfork则可能出现误报,关于误报的更多说明可以参阅下面两个issue:

检测全局缓冲区溢出

// gbo.c
#include <stdio.h>

int global_array[100] = {-1};

int main(int argc, char **argv) {
  global_array[101] = 1;
  return 0;
}

上面的代码与栈溢出案例的代码相似,不同仅仅只是的是我们在全局数据段上创建了一个容量为100的数组,接下来添加-fsanitize=address -g参数构建代码并执行:

~/Code/test$ g++ gbo.c -o gbo -fsanitize=address -g
~/Code/test$ ./gbo 
=================================================================
==2213117==ERROR: AddressSanitizer: global-buffer-overflow on address 0x558855e231b4 at pc 0x558855e20216 bp 0x7ffd9569d280 sp 0x7ffd9569d270
WRITE of size 4 at 0x558855e231b4 thread T0
    #0 0x558855e20215 in main /home/chenbing/Code/test/gbo.c:7
    #1 0x7efd3da4f082 in __libc_start_main ../csu/libc-start.c:308
    #2 0x558855e2010d in _start (/home/chenbing/Code/test/gbo+0x110d)

0x558855e231b4 is located 4 bytes to the right of global variable 'global_array' defined in 'gbo.c:4:5' (0x558855e23020) of size 400
SUMMARY: AddressSanitizer: global-buffer-overflow /home/chenbing/Code/test/gbo.c:7 in main
Shadow bytes around the buggy address:
  0x0ab18abbc5e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0ab18abbc5f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0ab18abbc600: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0ab18abbc610: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0ab18abbc620: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0ab18abbc630: 00 00 00 00 00 00[f9]f9 f9 f9 f9 f9 00 00 00 00
  0x0ab18abbc640: f9 f9 f9 f9 f9 f9 f9 f9 00 00 00 00 00 00 00 00
  0x0ab18abbc650: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0ab18abbc660: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0ab18abbc670: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0ab18abbc680: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASAN internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
  Shadow gap:              cc
==2213117==ABORTING

上面的报告基本与栈溢出案例的报告相同,不同的只是错误类型和全局对象代码位置的报告方式,这里不再过多介绍。

好了,关于 ASAN 的使用案例我们就介绍到这里,更多内容可以自行到ASAN的项目中去寻找

ASAN 的基本原理

ASAN的内存检测方法与Valgrind的AddrCheck工具很像,都是使用shadow内存来记录应用程序的每个字节是否可以被安全的访问,在访问内存时都对其映射的shadow内存进行检查。

但是,ASAN使用一个更具效率的shadow内存映射机制和更加紧凑的内存编码来实现,并且除了堆内存外还能检测栈和全局对象中的错误访问,且比AddrCheck快一个数量级。

ASAN由两部分组成:代码插桩模块和运行时库。

  • 代码插桩模块会修改代码使其在访问内存时检查每块内存访问状态,称为shadow 状态,以及在内存两侧创建redzone的内存区域。
  • 运行时库则提供一组接口用来替代malloc和free以及相关的函数,使得在分配堆空间时在其周围创建redzone,并在内存出错时报告错误。

首先,我们先介绍一下什么是shadow 内存 和 redzone。

  • shadow 内存

    在ASAN中malloc函数返回的内存地址通常至少是8个字节对齐,比如malloc(15)将分配得到2块大小为8字节的内存,在这个场景中,第二块8字节内存的前5个字节是可以访问,但剩下的3个字节是不能访问的。

    所谓的shadow 内存就是在应用程序的虚拟地址空间中预留一段地址空间,用来存储映射应用程序访问的内存块中哪些字节可以被使用的信息,这些信息就是shadow 状态。其中每1个字节的shadow 内存,映射到8个字节的应用程序内存,因此,shadow状态可能有3种:

    1. 0: 表示映射的8个字节均可以使用

    2. k(1<=k<=7): 表示表示映射的8个字节中只有前k个字节可以使用

    3. 负值: 表示映射的8个字节均不可使用,且不同的值表示所映射不同的内存类型(堆、栈、全局对象或已释放内存)

      ASAN使用带有比例和偏移量的直接映射将应用程序地址转换为其对应的shadow内存地址:

      shadow_address = (addr >> 3) + offset

      假设max - 1是虚拟地址空间中的最大有效地址,则offset的值应选择为在启动时不被占用的从offset到offset+Max/8的区域。

    • 在 32 位 linux 系统中,虚拟地址空间为:0x00000000-0xffffffff,offset = 0x20000000(2^29)。
    • 在 64 位系统中,ofsset = 0x0000100000000000(2^44)。
    • 在某些情况下(例如,在 Linux 上使用 -fPIE/-pie 编译器标志)可以使用零偏移来进一步简化检测。

    以下是 32 位 linux 系统中的地址空间分布

    0x1 0000 0000 ---------------
                  |   HIGH      |
                  |   MEMORY    |
      0x4000 0000 ---------------
                  | HIGH SHADOW |
      0x2800 0000 ---------------
                  | BAD REGION  |
      0x2400 0000 ---------------
                  | LOW SHADOW  |
      0x2000 0000 ---------------
                  | LOW MEMORY  |
      0x0000 0000 ---------------
    

    虚拟地址空间被划分为高低两部分,每个部分的内存地址映射到相应的shadow 内存。注意:将shadow 内存中的地址进行映射会得到Bad 区域中的地址,Bad 区域是被页面保护标记为不可访问的地址空间。

    shadow映射方式可以推导为(addr >> scale) + offset的形式,其中scale是的取值范围是1~7,当 scale=N时,shadow 内存占用虚拟地址空间的1/2^N, red-zone的最小大小为2^N字节(保证malloc()的对齐要求)。shadow 内存中的每个字节描述了2^N个内存字节的状态并有2^N + 1个不同的值。

  • redzone

    ASAN会在应用程序使用的堆、栈、全局对象的内存周围分配额外内存,这个额外的内存叫做redzone,redzone会被shadow 内存标记为不可使用状态,当应用程序访问redzone内存时说明已经溢出访问了,此时,ASAN检测redzone的shadow 状态后就会报告相应错误。readzone越大,检测内存下溢和上溢的范围越大。具体的分配策略将在下面涉及。

代码插桩

ASAN 会在应用程序访问内存的位置进行插桩,对于访问完整8字节内存的位置,插入以下代码检查内存对应的 shadow 内存,以此判断是否访问异常:

ShadowAddr = (Addr >> 3) + Offset;

if (*ShadowAddr != 0)
  ReportAndCrash(Addr);

由于应用程序访问8字节的内存,因此,其映射的shadow 内存的存储值必须是0,表示该8字节内存完全可用,否则,报错。

应用程序对 1、2、或者 4 字节内存的访问要复杂一些,如果访问的内存块对应的shadow 内存的存储值如果不是负数,且不为0,或者将要访问内存块超过了shadow 内存表示的可用范围,意味着本次将访问到不可使用的内存:

ShadowAddr = (Addr >> 3) + Offset;
k = *ShadowAddr;
if (k != 0 && ((Addr & 7) + AccessSize > k))
  ReportAndCrash(Addr);

需要注意的是,ASAN对源代码的插桩时机是在LLVM对代码编译优化之后,也就意味着ASAN只能检测 LLVM 优化后幸存下来的内存访问,例如:被 LLVM 优化掉的对栈对象进行访问的代码将不会被ASAN所识别。

同时,ASAN也不会对 LLVM 生成的内存访问代码进行插桩,例如:寄存器溢出检查等等。

另外,即使错误报告代码ReportAndCrash(Addr)只会被调用一次,但由于会在代码中的许多位置进行插入,因此,错误报告代码也必须相当紧凑。

目前 ASAN 使用了一个简单的函数调用来处理错误报告,当然还有另一个选择是插入一个硬件异常。

运行时库

在应用程序启动时,将映射整个shadow 内存,因此程序的其他部分不能使用它。BAD 区域也是受保护的,应用程序也不能访问。

在 linux 操作系统中,shadow 内存区域不会被占用,因此,映射总是成功的。但在 MacOS 中可能需要禁用地址空间布局(ASLR)。

另外,根据 GOOGLE 工程师介绍,shadow 内存区域的布局也适用于 windows 操作系统。

启用 ASAN 时,源代码中的 malloc 和 free 函数将会被替换为运行时库中的 malloc 和 free 函数。

malloc 分配的内存区域被组织为为一个与对象大小相对应的空闲列表数组。当对应于所请求内存大小的空闲列表为空时,从操作系统(例如,使用mmap)分配带有redzone的内存区域。n个内存块,将分配n+1个redzone:

| redzone-1 | memory-1 | redzone-2 | memory-2 | redzone-3 |

free 函数会将整个内存区域置成不可使用并将其放入隔离区,这样该区域就不会马上被 malloc 分配给应用程序。

目前,隔离区是使用一个 FIFO 队列实现的,它在任何时候都拥有一定数量的内存。

默认情况下,malloc 和 free 记录当前调用堆栈,以便提供更多信息的错误报告。 malloc 调用堆栈存储在左侧 redzone 中(redzone 越大,可以存储的帧数越多),而 free 调用堆栈存储在内存区域本身的开头。

到这里你应该已经明白了对于动态分配的内存,ASAN是怎么实现检测的,但你可能会产生疑惑:动态分配是通过 malloc 函数分配redzone来支持错误检测,那栈对象和全局对象这类没有malloc分类内存的对象是怎么实现的呢?其实原理也很简单:

  • 对于全局变量,redzone 在编译时创建,redzone 的地址在应用程序启动时传递给运行时库。 运行时库函数会将redzone 设置为不可使用并记录地址以供进一步错误报告。

  • 对于栈对象,redzone 是在运行时创建和置为不可使用。 目前,使用32字节的 redzone。例如以下代码片段:

    void foo() {
      char a[10];
      <function body> 
    }

    经 ASAN 处理后的代码大致如下:

    void foo() {
      char rz1[32]
      char arr[10];
      char rz2[32-10+32];
    
      unsigned * shadow = (unsigned*)(((long)rz1>>8)+Offset);
    
      // 将 redzone 设置为不可使用
      shadow[0] = 0xffffffff; // rz1
      shadow[1] = 0xffff0200; // arr and rz2
      shadow[2] = 0xffffffff; // rz2
    
      <function body>
    
      // 将所有内存设置成可以使用
      shadow[0] = shadow[1] = shadow[2] = 0; 
    }

总结

ASAN 使用shadow 内存和redzone来提供准确和即时的错误检测。

传统观点认为,shadow 内存和redzone要么通过多级映射方案产生高开销,要么占用大量的程序内存。但,ASAN的使用的shadow映射机制和shadow 状态编码减少了对内存空间占用。

最后,如果你觉得ASAN插桩代码和检测的对你某些的代码来说太慢了,那么可以使用编译器标志来禁用特定函数的,使ASAN跳过对代码中某个函数的插桩和检测, 跳过分析函数的编译器指令是:

__attribute__((no_sanitize_address))

Enjoy GreatSQL :)

文章推荐:

面向金融级应用的GreatSQL正式开源 https://mp.weixin.qq.com/s/cI_wPKQJuXItVWpOx_yNTg

Changes in GreatSQL 8.0.25 (2021-8-18) https://mp.weixin.qq.com/s/qcn0lmsMoLtaGO9hbpnhVg

MGR及GreatSQL资源汇总 https://mp.weixin.qq.com/s/qXMct_pOVN5FGoLsXSD0MA

GreatSQL MGR FAQ https://mp.weixin.qq.com/s/J6wkUpGXw3YkyEUJXiZ9xA

在Linux下源码编译安装GreatSQL/MySQL https://mp.weixin.qq.com/s/WZZOWKqSaGSy-mpD2GdNcA

关于 GreatSQL

GreatSQL是由万里数据库维护的MySQL分支,专注于提升MGR可靠性及性能,支持InnoDB并行查询特性,是适用于金融级应用的MySQL分支版本。

Gitee:

https://gitee.com/GreatSQL/GreatSQL

GitHub:

https://github.com/GreatSQL/GreatSQL

Bilibili:

https://space.bilibili.com/1363850082/video

微信&QQ群:

可搜索添加GreatSQL社区助手微信好友,发送验证信息“加群”加入GreatSQL/MGR交流微信群

QQ群:533341697

微信小助手:wanlidbc

本文由博客一文多发平台 OpenWrite 发布!

VS2019集成AddressSanitizer:C++内存错误检测实战指南 在C++开发中,内存管理是核心挑战之一,涉及堆、栈和全局变量的分配与释放。其原理在于程序运行时对内存地址的访问必须严格遵循分配边界和生命周期规则,否则会导致缓冲区溢出、释放后使用等严重错误。这类问题的技术价值在于直接影响程序的稳定性、安全性和性能,是高质量软件必须攻克的基础难题。应用场景广泛,从嵌入式系统到大型服务器软件,都需要可靠的内存错误检测机制。AddressSanitizer(ASan)作为一种编译时插桩技术,通过影子内存和运行时检查,能精准定位堆缓冲区溢出、释放后使用等隐蔽错误。本文以Visual 阅读详情

相关推荐

GCC内存检测神器:AddressSanitizer实战指南(附常见内存错误排查)

本文详细介绍了GCC工具链中的内存检测神器AddressSanitizer(ASan),包括其核心原理、配置方法和实战应用。通过典型内存错误案例,如堆栈越界访问、使用已释放内存等,演示了ASan如何快速定位和修复问题。文章还提供了性能优化建议和高级技巧,帮助开发者高效排查内存错误。

weixin_28309723的博客 340

内存错误分析工具----asan(AddressSanitizer)的介绍和使用

内存错误分析工具----asan(AddressSanitizer)的介绍和使用

C/C++、嵌入式开发、深度学习算法、模型部署与推理优化 3万+

Linux下AddressSanitizer(ASan)编译通过但无法运行的排查指南

内存安全是C/C++开发中的核心挑战,野指针、缓冲区溢出等内存错误常导致程序崩溃或安全漏洞。AddressSanitizer(ASan)作为编译时插桩工具,通过影子内存机制实时检测内存访问违规,其原理是在程序地址空间建立映射关系,每8字节应用内存对应1字节影子内存标记状态。这项技术能显著提升内存问题定位效率,在Linux开发、安全测试和持续集成场景中价值突出。然而实际部署时,常遇到程序编译通过却无法启动的问题,这通常涉及动态库依赖、环境变量冲突或工具链兼容性等工程实践环节。本文针对libasan.so缺失、

weixin_30387423的博客 304

内存调试神器- ASan详解及实例分析

内存调试神器- ASan详解及实例分析

深耕嵌入式领用多年, 致力于分享嵌入式领域技术! 1万+

5分钟搞定GCC/Clang内存检测:从编译选项到崩溃分析全流程

本文详细介绍了如何使用GCC/Clang的AddressSanitizer(ASan)工具快速检测C/C++程序中的内存错误。通过简单的编译选项-fsanitize=address,开发者可以高效地发现内存泄漏、内存越界等常见问题,并提供从编译、运行到错误分析的全流程实战指南,相比传统工具Valgrind性能开销更低。

weixin_29014939的博客 214

(GCC) gcc 编译选项 -fno-omit-frame-pointer,-fno-tree-vectorize,fno-optimize-sibling-calls;及内存泄漏、非法访问检测 A...

omit-frame-pointer 开启该选项,主要是用于去掉所有函数SFP(Stack Frame Pointer)的,即在函数调用时不保存栈帧指针SFP,代价是不能通过backtrace进行调试根据堆栈信息了。通过去掉SFP,可以提高程序运行速度,达到优化程序的目的。如果要打开栈指针,使用-fno-omit-frame-pointer。 另外一个类似选项-fomit-leaf-fram...

SurpassLi的专栏 510

内存泄漏终结者来了,Clang 17中你不可不知的3大调试神器

快速定位内存问题,Clang 17调试工具带来全新解决方案。涵盖AddressSanitizer增强、静态分析改进与实时错误追踪,适用于C/C++开发中的内存泄漏和越界访问检测,提升调试效率。三大神器助力高效开发,值得收藏。

PixelWander的博客 1013

Linux如何调试内存泄漏

内存泄漏是指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,...

编程与实战的博客 1363

用ASAN进行压测

用ASAN进行压测

yumao1的专栏 343

ASAN_OPTIONS for AddressSanitizer (ASan)

是运行时配置的环境变量,它控制着 ASan 检测器在程序运行时的各种行为。

我的博客 157

麒麟 ARMv10 SP3 安装 Oracle 19c 官方文档 或 教程

在麒麟ARMv10 SP3系统上安装Oracle 19c需要特别注意架构兼容性。

qq_43032338的博客 2938

内存检测神器 ASan(AddressSanitizer) 原理、编译配置与工程实战

当程序运行时,ASan会监测内存访问,一旦发现内存访问错误,比如越界访问、释放后再次访问等,会立即输出错误信息并中断程序执行,同时提供详细的报告帮助开发者定位问题的源头。通过使用ASan,开发者可以在早期阶段发现和解决潜在的内存错误问题,有效提高程序的稳定性和安全性。,目的是帮助开发者检测和调试内存相关的问题,如使用未分配的内存、使用已释放的内存、堆内存溢出等。编译完成后,运行生成的可执行文件,ASan会在运行时监测程序的内存访问情况,并在发现错误时提供详细的错误信息,包括错误的位置和类型。

谢谢大家的关注和点赞!这里只有纯纯的知识干货,没有一句废话。希望能实实在在帮到大家~要是觉得有用,别忘了给我点支持哟,你 225

Tesla V100 在 Windows 下的驱动选择与模式切换实战

本文详细解析了Tesla V100专业计算卡在Windows系统下的驱动安装与模式切换实战。针对设备管理器识别但任务管理器“隐身”的常见问题,文章指出其默认TCC模式为纯计算设计,并指导用户正确选择数据中心或GRID驱动。核心在于通过注册表修改或驱动选择,在高效的TCC模式与便于监控的WDDM模式间切换,同时提供了nvidia-smi命令行监控与AI框架测试等稳定使用方案。

weixin_29247219的博客 1804

VS2019 16.9+ AddressSanitizer 实战:3步开启C++内存检测,捕获5类常见错误

本文详细介绍了如何在VS2019 16.9+中使用AddressSanitizer(ASan)进行C++内存检测,通过三步配置快速启用ASan,精准捕获堆栈缓冲区溢出、释放后使用等5类常见内存错误。ASan作为Windows平台的高效内存检测工具,具有检测全面、性能开销低等优势,是C++开发者提升代码质量的利器。

weixin_33700809的博客 310

四旋翼滑模控制:从理论到Simulink实现

本文深入探讨了四旋翼滑模控制的理论与Simulink实现,对比了其相较于PID控制在抗干扰和鲁棒性方面的显著优势。文章详细介绍了从精确动力学建模、内外环滑模控制器设计到指令解算的完整流程,并分享了Simulink仿真搭建、参数调试及抗扰性能验证的实战经验,帮助工程师掌握在复杂环境下提升四旋翼稳定性的进阶技能。

weixin_29163581的博客 375

SAP SD核心流程解析:从销售订单到财务结算的实战指南

本文深入解析SAP SD模块从销售订单到财务结算的核心流程,重点剖析了销售订单中的行项目类别如何作为流程的“总开关”,决定了后续交货、开票及财务集成的走向。文章结合实战案例,详细拆解了正常销售、退货、第三方销售、寄售等典型流程的配置要点与国内化适配方案,并阐述了SD与财务模块集成的关键连接点,为顾问和用户提供了一份全面的实战指南。

weixin_29250403的博客 426

C/C++与ARM嵌入式内存管理:从原理到实战解决内存泄漏与溢出

内存管理是计算机系统编程的核心基础,它涉及程序运行时数据的存储、访问与释放机制。其原理基于操作系统提供的虚拟内存空间划分,包括栈、堆、数据段等不同区域,各自承担着自动变量、动态分配和全局数据等职责。掌握内存管理的技术价值在于能编写出高效、稳定且安全的程序,避免资源耗尽和未定义行为。在应用场景上,无论是开发高性能服务器、系统底层工具,还是进行ARM嵌入式设备编程,精准的内存控制都至关重要。本文聚焦于C/C++语言和ARM平台,深入探讨**内存泄漏**与**缓冲区溢出**这两类常见问题的成因、危害及系统性解决方

weixin_34130389的博客 461

FDTD Solutions-入射波长模式

经常在文献中看到仿真光源使用TM/TE模式,这个确定是十分必要的,决定了仿真结果。 但是也不能局限于选择TM/TE,因为不同领域、不同软件之间的使用习惯和命名可能不同。并且在现代光波导器件中,几乎全部 都是混合模式,或者称之为quasi-TE(准TE模式)和quasi-TM(准TM模式),多数情况下都简称为TE模式和TM模式。要知道 到底是TE模式还是TM模式,需要查看其电场的主分量(即振幅...

HECHUANWANG的博客 1万+

Google Address Sanitizer 学习

AddressSanitizer(ASan)是google开发的一个应用内存检查工具,性能据说比valgrind要好不少,可以配合clang或者GCC编译器使用,GCC需要4.8及以上版本。4.8版本GCC对AddressSanitizer支持有限,功能不太完善,输出的错误信息也不够友好,使用不太方便,建议使用4.9及以上版本。C/C++ 内存治理神器 - Google SanitizersAddressSanitizer(又名ASan)是C/C++的内存错误检测器。

mayue_web的博客 1671

YOLOv11改进策略【独家融合改进】| MobileNetV4+BiFPN,轻松实现降参涨点,适用专栏内所有的骨干替换

可扩展的高效物体检测BiFPN(加权双向特征金字塔网络)

Limiiiing的博客 2358

ESP32-C3入门教程 蓝牙篇①——基于Blufi 的 WiFi 智能配网

文章目录一、前言二、WiFi配网方式三、快速运行四、运行效果五、ESP32-C3程序流程5.1 主程序5.2 回调函数5.3 SmartConfig线程5.4 流程图六、关键函数6.1 设置SmartConfig的协议类型6.2 启动SmartConfig6.3 停止SmartConfig七、其他八、参考 一、前言 本文基于VS Code IDE进行编程、编译、下载、运行等操作 基础知识点请查阅:ESP32-C3入门教程 基础篇①——基于VS Code构建Hello World 二、WiFi配网方式 W

7764

C/C++ 安全开发入门:缓冲区溢出、UAF、内存越界,漏洞成因与编码防护

C/C++ 是系统级开发的主力语言,操作系统、驱动、中间件、音视频引擎、通信协议栈、嵌入式固件、游戏引擎大量使用 C/C++ 开发。C/C++ 语言直接管理内存,没有内置边界检查、没有垃圾回收,是内存安全漏洞的高发语言。缓冲区溢出、UAF(释放后使用)、内存越界、空指针解引用、整数溢出、堆溢出,构成了大量 CVE 漏洞的核心成因。在 SRC 挖洞、二进制安全、漏洞挖掘(PWN/CTF)、安全开发(SDL/DevSecOps)领域,C/C++ 代码审计是基础中的基础。

Button12138的博客 238

SFT看这一篇就够了!万字长文:监督微调(Supervised Fine-Tuning, SFT)技术全解析【从理论到实战】

大家好,我是大 F,深耕AI算法十余年,互联网大厂技术岗。分享AI算法干货、技术心得。欢迎关注,一起探索技术的无限可能!

大F子的智能小课 711
上一篇: 使用 Vagrant 在 VirtualBox 安装 Linux 虚拟机
下一篇: GreatSQL FAQ
GreatSQL社区
博客等级 码龄5年 1210粉丝 390原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值