文章目录

  • 一、 简介
  • 二、AddressSanitizer 的使用
    • 使用方法
    • 1.使用添加编译选项的方式使用ASan
    • 2.使用CMake添加编译选项
  • 三、测试
      • 不添加Asan选项,不会有任何输出
      • 添加-fsanitize=address选项:
      • 简单修改程序,释放申请的内存空间:
  • 四、不能在gdb、lldb等调试器运行带有AddressSanitizer的程序
  • 参考

一、 简介

所谓工欲善其事,必先利其器。在linux系统下进行C++的开发时,出现内存访问错误是非常常见的事情,且大多数情况下gcc和gdb等编译器和调试器都不会提示我们发生内存错误,通常会导致严重的后果且难以定位问题。

AddressSanitizer (ASan)是一个性能非常好的 C/C++ 内存错误探测工具。它由编译器的插桩模块(目前,LLVM 通过)和替换了 malloc 函数的运行时库组成。这个工具可以探测如下这些类型的错误:

  • 对堆,栈和全局内存的访问越界(堆缓冲区溢出,栈缓冲区溢出,和全局缓冲区溢出)
  • UAP(Use-after-free,悬挂指针的解引用,或者说野指针)
  • Use-after-return(无效的栈上内存,运行时标记
  • ASAN_OPTIONS=detect_stack_use_after_return=1)
  • Use-After-Scope(作用域外访问,clang 标记 -fsanitize- address-use-after-scope )
  • 内存的重复释放
  • 初始化顺序的 bug
  • 内存泄漏

这个工具非常快。通常情况下,内存问题探测这类调试工具的引入,会导致原有应用程序运行性能的大幅下降,比如大名鼎鼎的 valgrind 据说会导致应用程序性能下降到正常情况的十几分之一,但引入 AddressSanitizer 只会减慢运行速度的一半。
使用 AddressSanitizer 可以在程序发生内存问题的时候及时检查出来,精准定位发生内存问题的位置,大大提高我们debug的效率。


二、AddressSanitizer 的使用

自 LLVM 的版本 3.1 和 GCC 的版本 4.8 开始,AddressSanitizer 就是它们的一部分。因此只要版本正确,无需额外链接库或者编译源码,只需编译的时候加上选项即可使用。

使用方法

1.使用添加编译选项的方式使用ASan

  • 使用-fsanitize=address选项编译和链接你的程序。
    例如:
gcc -fsanitize=address -O1 -g -o  main main.cpp 
  • 如果有其他需要,加上-fno-omit-frame-pointer等其他编译选项
gcc -fsanitize=address -fno-omit-frame-pointer -O1 -g -o main main.cpp

可选择-O1或者更高的优化级别编译

2.使用CMake添加编译选项

在CMakeLists文件中添加如下行

set(CMAKE_CXX_FLAGS_DEBUG  "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=address")

对于AddressSanitizer还有很多编译选项,这里不一一列举,若有需要自行去官方文档查询。

三、测试

以下程序是有两个很明显的内存错误:

#include <iostream>
using namespace std;int main() {char *c = new char(10);c[1] = 'a';return 0;
}

不添加Asan选项,不会有任何输出

ubuntu@VM-24-14-ubuntu:~/CppTest$ g++ -O1 -g -o test test.cpp 
ubuntu@VM-24-14-ubuntu:~/CppTest$ ./test
ubuntu@VM-24-14-ubuntu:~/CppTest$ 

编译器的缄默不语是导致程序员痛苦整宿的元凶之一。

添加-fsanitize=address选项:

ubuntu@VM-24-14-ubuntu:~/CppTest$ g++ -fsanitize=address -O1 -g -o test test.cpp 
ubuntu@VM-24-14-ubuntu:~/CppTest$ ./test 
==175312==AddressSanitizer: libc interceptors initialized
|| `[0x10007fff8000, 0x7fffffffffff]` || HighMem    ||
|| `[0x02008fff7000, 0x10007fff7fff]` || HighShadow ||
|| `[0x00008fff7000, 0x02008fff6fff]` || ShadowGap  ||
|| `[0x00007fff8000, 0x00008fff6fff]` || LowShadow  ||
|| `[0x000000000000, 0x00007fff7fff]` || LowMem     ||
MemToShadow(shadow): 0x00008fff7000 0x000091ff6dff 0x004091ff6e00 0x02008fff6fff
redzone=16
max_redzone=2048
quarantine_size_mb=256M
thread_local_quarantine_size_kb=1024K
malloc_context_size=30
SHADOW_SCALE: 3
SHADOW_GRANULARITY: 8
SHADOW_OFFSET: 0x7fff8000
==175312==Installed the sigaction for signal 11
==175312==Installed the sigaction for signal 7
==175312==Installed the sigaction for signal 8
==175312==T0: stack [0x7ffc3e50a000,0x7ffc3ed0a000) size 0x800000; local=0x7ffc3ed08294
==175312==AddressSanitizer Init done
==175313==Processing thread 175312.
==175313==Stack at 0x7ffc3e50a000-0x7ffc3ed0a000 (SP = 0x7ffc3ed07e88).
==175313==TLS at 0x7fba89ae14c0-0x7fba89ae2580.=================================================================
==175312==ERROR: LeakSanitizer: detected memory leaksDirect leak of 1 byte(s) in 1 object(s) allocated from:#0 0x7fba8a165587 in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cc:104#1 0x55840905425a in main /home/ubuntu/CppTest/test.cpp:5SUMMARY: AddressSanitizer: 1 byte(s) leaked in 1 allocation(s).
ubuntu@VM-24-14-ubuntu:~/CppTest$ 

提示检测到第五行出现内存泄漏问题。原来是源程序new了一个对象,并没有delete。

简单修改程序,释放申请的内存空间:

#include <iostream>
using namespace std;int main() {char *c = new char(10);c[1] = 'a';delete c;return 0;
}

再次编译运行:

ubuntu@VM-24-14-ubuntu:~/CppTest$ g++ -fsanitize=address -O1 -g -o test test.cpp 
ubuntu@VM-24-14-ubuntu:~/CppTest$ ./test 
==175937==AddressSanitizer: libc interceptors initialized
|| `[0x10007fff8000, 0x7fffffffffff]` || HighMem    ||
|| `[0x02008fff7000, 0x10007fff7fff]` || HighShadow ||
|| `[0x00008fff7000, 0x02008fff6fff]` || ShadowGap  ||
|| `[0x00007fff8000, 0x00008fff6fff]` || LowShadow  ||
|| `[0x000000000000, 0x00007fff7fff]` || LowMem     ||
MemToShadow(shadow): 0x00008fff7000 0x000091ff6dff 0x004091ff6e00 0x02008fff6fff
redzone=16
max_redzone=2048
quarantine_size_mb=256M
thread_local_quarantine_size_kb=1024K
malloc_context_size=30
SHADOW_SCALE: 3
SHADOW_GRANULARITY: 8
SHADOW_OFFSET: 0x7fff8000
==175937==Installed the sigaction for signal 11
==175937==Installed the sigaction for signal 7
==175937==Installed the sigaction for signal 8
==175937==T0: stack [0x7ffc9bbc7000,0x7ffc9c3c7000) size 0x800000; local=0x7ffc9c3c5014
==175937==AddressSanitizer Init done
=================================================================
==175937==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000011 at pc 0x55ebd66cc2de bp 0x7ffc9c3c4ff0 sp 0x7ffc9c3c4fe0
WRITE of size 1 at 0x602000000011 thread T0#0 0x55ebd66cc2dd in main /home/ubuntu/CppTest/test.cpp:6#1 0x7fa58b097082 in __libc_start_main ../csu/libc-start.c:308#2 0x55ebd66cc1cd in _start (/home/ubuntu/CppTest/test+0x11cd)0x602000000011 is located 0 bytes to the right of 1-byte region [0x602000000010,0x602000000011)
allocated by thread T0 here:#0 0x7fa58b6c0587 in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cc:104#1 0x55ebd66cc29a in main /home/ubuntu/CppTest/test.cpp:5SUMMARY: AddressSanitizer: heap-buffer-overflow /home/ubuntu/CppTest/test.cpp:6 in main
Shadow bytes around the buggy address:0x0c047fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x0c047fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x0c047fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x0c047fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x0c047fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c047fff8000: fa fa[01]fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c047fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c047fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c047fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c047fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c047fff8050: 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:           00Partially addressable: 01 02 03 04 05 06 07 Heap left redzone:       faFreed heap region:       fdStack left redzone:      f1Stack mid redzone:       f2Stack right redzone:     f3Stack after return:      f5Stack use after scope:   f8Global redzone:          f9Global init order:       f6Poisoned by user:        f7Container overflow:      fcArray cookie:            acIntra object redzone:    bbASan internal:           feLeft alloca redzone:     caRight alloca redzone:    cbShadow gap:              cc
==175937==ABORTING
ubuntu@VM-24-14-ubuntu:~/CppTest$ 

这次提示又不一样了,提示第6行出现了heap-buffer-overflow。熟悉错误信息的同学很快就能意识到,这是提示程序出现了越界访问内存的错误。
通关这几次测试,我们可以发现,AddressSanitizer可以帮助我们快速定位编译器检测不出来的内存问题,大大提高了我们debug的效率。

四、不能在gdb、lldb等调试器运行带有AddressSanitizer的程序

笔者就是为了记录这个问题,特意包了这碗饺子。
当尝试使用gdb调试添加了AddressSanitizer编译选项的程序时,程序会拒绝执行,提示:

==163135==LeakSanitizer has encountered a fatal error.
==163135==HINT: For debugging, try setting environment variable LSAN_OPTIONS=verbosity=1:log_threads=1
==163135==HINT: LeakSanitizer does not work under ptrace (strace, gdb, etc)

根据网上找到的资料,LeakSanitizer在内部使用ptrace,可能会挂起所有线程,以便它可以扫描泄漏而不会产生误报。只有一个应用程序可以使用ptrace,因此,如果您在gdb或strace下运行应用程序,则LeakSanitizer将无法通过ptrace进行附加。

说人话就是,LINUX系统下只同时允许一个进程使用ptrace()这个系统调用,而ptrace是用于进程跟踪的,它提供了父进程可以观察和控制其子进程执行的能力,并允许父进程检查和替换子进程的内核镜像(包括寄存器)的值。
gdb等调试器也会使用ptrace,因此与LeakSanitizer相冲突,不允许执行进程。

参考

Address Sanitizer 用法
Linux 下的 AddressSanitizer
c++ Asan(address-sanitize)的配置和使用
Ptrace 详解