地址消毒器

介绍

地址消毒器是一个快速内存错误检测器。它包含一个编译器代码插桩模块和一个运行时库。该工具可以检测以下类型的错误

  • 堆、栈和全局变量的越界访问

  • 使用后释放

  • 返回后使用 (clang 标志 -fsanitize-address-use-after-return=(never|runtime|always) 默认值:runtime)
    • 使用 ASAN_OPTIONS=detect_stack_use_after_return=1 启用 (在 Linux 上已启用)。

    • 使用 ASAN_OPTIONS=detect_stack_use_after_return=0 禁用。

  • 范围后使用 (clang 标志 -fsanitize-address-use-after-scope)

  • 双重释放、无效释放

  • 内存泄漏 (实验性)

地址消毒器引入的典型性能下降为 2x

如何构建

使用 CMake 构建 LLVM/Clang 并启用 compiler-rt 运行时。以下是一个允许使用/测试地址消毒器的 CMake 配置示例

$ cmake -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS="clang" -DLLVM_ENABLE_RUNTIMES="compiler-rt" <path to source>/llvm

用法

只需使用 -fsanitize=address 标志编译和链接程序。地址消毒器运行时库应链接到最终的可执行文件,因此请确保使用 clang (而不是 ld) 进行最终链接步骤。链接共享库时,不会链接地址消毒器运行时,因此 -Wl,-z,defs 可能会导致链接错误 (不要与地址消毒器一起使用它)。为了获得合理的性能,请添加 -O1 或更高版本。为了在错误消息中获得更清晰的堆栈跟踪,请添加 -fno-omit-frame-pointer。为了获得完美的堆栈跟踪,您可能需要禁用内联 (只需使用 -O1) 和尾调用消除 (-fno-optimize-sibling-calls)。

% cat example_UseAfterFree.cc
int main(int argc, char **argv) {
  int *array = new int[100];
  delete [] array;
  return array[argc];  // BOOM
}

# Compile and link
% clang++ -O1 -g -fsanitize=address -fno-omit-frame-pointer example_UseAfterFree.cc

或者

# Compile
% clang++ -O1 -g -fsanitize=address -fno-omit-frame-pointer -c example_UseAfterFree.cc
# Link
% clang++ -g -fsanitize=address example_UseAfterFree.o

如果检测到错误,程序将向 stderr 打印错误消息并退出,退出代码非零。地址消毒器在检测到第一个错误时退出。这是有意为之

  • 这种方法允许地址消毒器生成更快、更小的代码 (两者都减少了约 5%)。

  • 修复错误变得不可避免。地址消毒器不会产生误报。一旦发生内存损坏,程序就处于不一致状态,这会导致混乱的结果并可能导致后续报告产生误导。

如果您的进程被沙箱化,并且您运行的是 OS X 10.10 或更早版本,则需要设置 DYLD_INSERT_LIBRARIES 环境变量,并将其指向与用于构建可执行文件的编译器打包在一起的 ASan 库。(您可以通过搜索名称中包含 asan 的动态库来找到库。)如果未设置环境变量,进程将尝试重新执行。另外请记住,将可执行文件移动到其他机器时,还需要将 ASan 库复制过去。

符号化报告

为了使地址消毒器符号化其输出,您需要将 ASAN_SYMBOLIZER_PATH 环境变量设置为指向 llvm-symbolizer 二进制文件 (或确保 llvm-symbolizer 位于您的 $PATH 中)

% ASAN_SYMBOLIZER_PATH=/usr/local/bin/llvm-symbolizer ./a.out
==9442== ERROR: AddressSanitizer heap-use-after-free on address 0x7f7ddab8c084 at pc 0x403c8c bp 0x7fff87fb82d0 sp 0x7fff87fb82c8
READ of size 4 at 0x7f7ddab8c084 thread T0
    #0 0x403c8c in main example_UseAfterFree.cc:4
    #1 0x7f7ddabcac4d in __libc_start_main ??:0
0x7f7ddab8c084 is located 4 bytes inside of 400-byte region [0x7f7ddab8c080,0x7f7ddab8c210)
freed by thread T0 here:
    #0 0x404704 in operator delete[](void*) ??:0
    #1 0x403c53 in main example_UseAfterFree.cc:4
    #2 0x7f7ddabcac4d in __libc_start_main ??:0
previously allocated by thread T0 here:
    #0 0x404544 in operator new[](unsigned long) ??:0
    #1 0x403c43 in main example_UseAfterFree.cc:2
    #2 0x7f7ddabcac4d in __libc_start_main ??:0
==9442== ABORTING

如果您无法正常使用 (例如,您的进程被沙箱化),可以使用单独的脚本离线符号化结果 (可以使用 ASAN_OPTIONS=symbolize=0 强制禁用在线符号化)

% ASAN_OPTIONS=symbolize=0 ./a.out 2> log
% projects/compiler-rt/lib/asan/scripts/asan_symbolize.py / < log | c++filt
==9442== ERROR: AddressSanitizer heap-use-after-free on address 0x7f7ddab8c084 at pc 0x403c8c bp 0x7fff87fb82d0 sp 0x7fff87fb82c8
READ of size 4 at 0x7f7ddab8c084 thread T0
    #0 0x403c8c in main example_UseAfterFree.cc:4
    #1 0x7f7ddabcac4d in __libc_start_main ??:0
...

请注意,在 macOS 上,您可能需要在二进制文件上运行 dsymutil 才能在地址消毒器报告中包含文件:行信息。

其他检查

初始化顺序检查

地址消毒器可以选择检测动态初始化顺序问题,即当一个翻译单元中定义的全局变量的初始化使用另一个翻译单元中定义的全局变量时。要启用运行时的此检查,您应该设置环境变量 ASAN_OPTIONS=check_initialization_order=1

请注意,此选项在 macOS 上不受支持。

返回后堆栈使用 (UAR)

地址消毒器可以选择检测返回后堆栈使用问题。默认情况下,或明确地 (-fsanitize-address-use-after-return=runtime) 可用。要禁用运行时的此检查,请设置环境变量 ASAN_OPTIONS=detect_stack_use_after_return=0

启用此检查 (-fsanitize-address-use-after-return=always) 将减小代码大小。通过完全消除此检查 (-fsanitize-address-use-after-return=never) 可以进一步减小代码大小。

总结一下:-fsanitize-address-use-after-return=<mode>
  • never:完全禁用 UAR 错误的检测 (减小代码大小)。

  • runtime:添加检测代码,但可以通过运行时环境禁用 (ASAN_OPTIONS=detect_stack_use_after_return=0)。

  • always:在所有情况下启用 UAR 错误的检测。(减小代码大小,但不如 never 那么大)。

内存泄漏检测

有关地址消毒器中泄漏检测器的更多信息,请参见 LeakSanitizer。泄漏检测默认情况下在 Linux 上开启,可以使用 ASAN_OPTIONS=detect_leaks=1 在 macOS 上开启;但是,它目前在其他平台上尚不支持。

问题抑制

地址消毒器预计不会产生误报。如果您看到误报,请再仔细检查一下;很可能是真阳性!

抑制外部库中的报告

运行时插入允许地址消毒器查找未重新编译代码中的错误。如果您在外部库中遇到问题,我们建议立即将其报告给库维护人员,以便他们解决。但是,您可以使用以下抑制机制来解除阻塞并继续测试。此抑制机制仅应用于抑制外部代码中的问题;它不适用于使用地址消毒器重新编译的代码。要抑制外部库中的错误,请将 ASAN_OPTIONS 环境变量设置为指向抑制文件。您可以指定文件的完整路径或相对于可执行文件位置的文件路径。

ASAN_OPTIONS=suppressions=MyASan.supp

使用以下格式指定要抑制的函数或库的名称。您可以在错误报告中看到这些名称。请记住,抑制范围越小,您就能发现越多的错误。

interceptor_via_fun:NameOfCFunctionToSuppress
interceptor_via_fun:-[ClassName objCMethodToSuppress:]
interceptor_via_lib:NameOfTheLibraryToSuppress

使用 __has_feature(address_sanitizer) 进行条件编译

在某些情况下,可能需要根据是否启用了地址消毒器执行不同的代码。为此目的,可以使用 __has_feature

#if defined(__has_feature)
#  if __has_feature(address_sanitizer)
// code that builds only under AddressSanitizer
#  endif
#endif

使用 __attribute__((no_sanitize("address"))) 禁用代码插桩

某些代码不应由地址消毒器进行代码插桩。可以使用属性 __attribute__((no_sanitize("address"))) (它具有已弃用的同义词 no_sanitize_addressno_address_safety_analysis) 来禁用特定函数的代码插桩。此属性可能不受其他编译器的支持,因此建议将其与 __has_feature(address_sanitizer) 一起使用。

在全局变量上使用的同一属性会阻止地址消毒器在该变量周围添加红区,从而检测越界访问。

地址消毒器还支持 __attribute__((disable_sanitizer_instrumentation))。此属性的工作方式类似于 __attribute__((no_sanitize("address"))),但它还会阻止其他消毒器执行的代码插桩。

抑制重新编译代码中的错误 (忽略列表)

地址消毒器支持 消毒器特殊情况列表 中的 srcfun 实体类型,可用于抑制指定源文件或函数中的错误报告。此外,地址消毒器引入了 globaltype 实体类型,可用于抑制对具有特定名称和类型的全局变量的越界访问的错误报告 (您只能指定类或结构体类型)。

您可以使用 init 类别来抑制有关在特定源文件中或与特定全局变量相关的初始化顺序问题发生的报告。

# Suppress error reports for code in a file or in a function:
src:bad_file.cpp
# Ignore all functions with names containing MyFooBar:
fun:*MyFooBar*
# Disable out-of-bound checks for global:
global:bad_array
# Disable out-of-bound checks for global instances of a given class ...
type:Namespace::BadClassName
# ... or a given struct. Use wildcard to deal with anonymous namespace.
type:Namespace2::*::BadStructName
# Disable initialization-order checks for globals:
global:bad_init_global=init
type:*BadInitClassSubstring*=init
src:bad/init/files/*=init

抑制内存泄漏

LeakSanitizer (如果作为 AddressSanitizer 的一部分运行)生成的内存泄漏报告可以通过作为

LSAN_OPTIONS=suppressions=MyLSan.supp

传递的单独文件抑制,该文件包含 leak:<pattern> 格式的行。如果模式与泄漏报告符号化堆栈跟踪中的任何函数名、源文件名或库名匹配,则内存泄漏将被抑制。有关更多详细信息,请参阅 完整文档

代码生成控制

代码插桩概述

默认情况下,AddressSanitizer 会内联代码插桩以提高运行时性能,这会导致二进制文件大小增加。使用(clang 标志 -fsanitize-address-outline-instrumentation` default: ``false)标志强制所有代码插桩被概述,这会减小生成的代码大小,但也会降低运行时性能。

限制

  • AddressSanitizer 比原生运行使用更多的实际内存。确切的开销取决于分配大小。分配越小,开销就越大。

  • AddressSanitizer 使用更多的堆栈内存。我们已经看到高达 3 倍的增长。

  • 在 64 位平台上,AddressSanitizer 映射(但不会保留)16+ TB 的虚拟地址空间。这意味着像 ulimit 这样的工具可能无法按预期工作。

  • 不支持可执行文件的静态链接。

安全注意事项

AddressSanitizer 是一种错误检测工具,其运行时不打算与生产可执行文件链接。虽然它可能对测试有用,但 AddressSanitizer 的运行时并非出于安全敏感约束而开发,可能会影响生成的执行文件的安全性。

支持的平台

AddressSanitizer 支持以下平台:

  • Linux i386/x86_64(在 Ubuntu 12.04 上测试)

  • macOS 10.7 - 10.11 (i386/x86_64)

  • iOS 模拟器

  • Android ARM

  • NetBSD i386/x86_64

  • FreeBSD i386/x86_64(在 FreeBSD 11-current 上测试)

  • Windows 8.1+ (i386/x86_64)

正在移植到其他各种平台。

当前状态

从 LLVM 3.1 开始,AddressSanitizer 在支持的平台上完全正常工作。测试套件已集成到 CMake 构建中,可以使用 make check-asan 命令运行。

Windows 端口功能齐全,并被 Chrome 和 Firefox 使用,但其支持程度不如其他端口。

更多信息

https://github.com/google/sanitizers/wiki/AddressSanitizer