clang之MemorySanitizer

3,486 阅读6分钟

Clang 12 documentation

Clang 12 documentation包含了一系列工具,如 AddressSanitizerThreadSanitizerLeakSanitizerLibTooling等。

  1. clang之AddressSanitizer
  2. clang之MemorySanitizer
  3. clang之LeakSanitizer
  4. clang之UndefinedBehaviorSanitizer
  5. clang之Hardware-assisted-AddressSanitizer
  6. clang之SafeStack
  7. clang之ShadowCallStack
  8. clang之ThreadSanitizer
  9. clang之Thread-Safety-Analysis
  10. clang之DataFlowSanitizer

这部分是对clang文档 Clang 12 documentation MemorySanitizer 的翻译。仅供参考。

介绍

MemorySanitizer 是一个内存检测器,能够检测未初始化内存的读取操作。包含一个编译器插桩模块和一个运行时库。

通常情况下,MemorySanitizer 会带来3x的性能损耗。

如何构建

使用 CMake 来构建 LLVM/Clang

用法

简单地使用 -fsanitize=memory 标记来编译链接代码即可。MemorySanitizerrun-time library 应该被链接到最终的可执行文件中,所以确保使用 clang(而非ld) 来执行最终的链接操作。当链接共享库的时候,MemorySanitizer runtime 库不会被链接。所以,-Wl, -z, defs 可能导致链接错误(不要跟 MemorySanitizer 一起使用)。使用 -O1 或更高的优化标记,可以获取更为合理的性能。使用 -fno-omit-frame-pointer ,可以在错误信息中提取更有意义的栈帧信息。为了获取完美的栈帧信息,可能需要禁用内联(仅使用 -O1)和跟踪调用消除(-fno-optimize-sibling-calls)(这里英文是 tail call elimination)。

% cat umr.cc
#include <stdio.h>

int main(int argc, char** argv) {
  int* a = new int[10];
  a[5] = 0;
  if (a[argc])
    printf("xx\n");
  return 0;
}

% clang -fsanitize=memory -fno-omit-frame-pointer -g -O2 umr.cc

如果检测到了一个bug,程序会打印出一个错误信息到 stderr,并且退出(退出状态码非0)。

% ./a.out
WARNING: MemorySanitizer: use-of-uninitialized-value
    #0 0x7f45944b418a in main umr.cc:6
    #1 0x7f45938b676c in __libc_start_main libc-start.c:226

默认情况下,MemorySanitizer 会在检测到第一个错误的时候退出。如果错误报告难以理解,尝试开启 origin tracking

__has_feature(memory_sanitizer)

在一些场景下,可能需要根据 MemorySanitizer 是否开启来执行不同的代码。__has_feature可以用来实现这一的目的。

#if defined(__has_feature)
#  if __has_feature(memory_sanitizer)
// code that builds only under MemorySanitizer
#  endif
#endif

attribute((no_sanitize("memory")))

有一些代码不应该使用 MemorySanitizer 来检测。在一些特定函数中,可以使用函数属性 no_sanitize("memory") 来禁用未初始化检查。 MemorySanitizer 依然需要插桩来避免错误提示。该属性在其他编译器中可能不支持,所以建议将其与 __has_feature(memory_sanitizer) 一起使用。

禁用名单

Sanitizer 的特定使用场景中,MemorySanitizer 支持 src and fun entity types ,可以用于针对特定源码文件和函数不要使用 MemorySanitizer 检查。所有的使用未初始化的警告都会单独剔除,所有从内存中加载的值都会被认为是已初始化过的。

符号化报告

MemorySanitizer 使用一个外部的符号化器,在报告中打印文件和行号信息。确保 llvm-symbolizer 二进制的路径已经被加入到了 $PATH 中,或者设置环境变量 MSAN_SYMBOLIZER_PATH 指向该路径。

来源追踪

MemorySanitizer 可以跟踪未初始化值的来源,类似于 Valgrind 中的 –track-origins 选项。该特性可以使用Clang选项 -fsanitize-memory-track-origins=2 (或者简单使用 -fsanitize-memory-track-origins)来开启。样例代码如下:

% cat umr2.cc
#include <stdio.h>

int main(int argc, char** argv) {
  int* a = new int[10];
  a[5] = 0;
  volatile int b = a[argc];
  if (b)
    printf("xx\n");
  return 0;
}

% clang -fsanitize=memory -fsanitize-memory-track-origins=2 -fno-omit-frame-pointer -g -O2 umr2.cc
% ./a.out
WARNING: MemorySanitizer: use-of-uninitialized-value
    #0 0x7f7893912f0b in main umr2.cc:7
    #1 0x7f789249b76c in __libc_start_main libc-start.c:226

  Uninitialized value was stored to memory at
    #0 0x7f78938b5c25 in __msan_chain_origin msan.cc:484
    #1 0x7f7893912ecd in main umr2.cc:6

  Uninitialized value was created by a heap allocation
    #0 0x7f7893901cbd in operator new[](unsigned long) msan_new_delete.cc:44
    #1 0x7f7893912e06 in main umr2.cc:4

默认情况下,MemorySanitizer 会收集未初始化值涉及到的内存分配以及所有的中间存储。Origin tracking 已被证明对于调试 MemorySanitizer 报告非常有用。它会降低程序执行效率(大概1.5x-2x on top of the usual MemorySanitizer slowdown),增加内存消耗。

开启Clang选项 -fsanitize-memory-track-origins=1 ,可以使得 MemorySanitizer 仅收集内存分配指针,而不收集中间存储(collects only allocation points but not intermediate stores)。这种模式相对轻量,运行更快一点。

对象被销毁后再使用的检测

可以在 MemorySanitizer 中使用正在实验阶段的 use-after-destruction (对象被销毁后再使用)检测工具。在析构函数被调用后,对象内存即被认为已经不可读了,如果继续使用该内存即会导致运行时的错误。

该特性依然处于实验阶段,如果需要在运行时期开启,需要如下步骤:

  1. 在编译阶段传递额外的Clang选项 -fsanitize-memory-use-after-dtor
  2. 在程序运行之前,设置环境变量 MSAN_OPTIONS=poison_in_dtor=1

处理外部代码

MemorySanitizer 要求程序的所有代码都被插桩。也会包含程序依赖的所有库,即便是 libc 库。如果不这样做,可能导致错误报告。因此,需要将所有对内存有写操作的内联的汇编代码,替换成纯 C/C++ 的代码。

完全的 MemorySanitizer 插桩比较难以完成。为了便于操作,MemorySanitizer 的运行时库包含了70+的拦截器,用于针对最普遍的 libc 函数。这样就可以在链接了未插桩 libc* 的情况下,运行 MemorySanitizer 插桩后的程序。例如,开发者可以使用 MemorySanitizer 插桩后的 Clang 编译器,即便在链接了他们自行开发的插桩后的 libc++ 库(用于替代 libstdc++ 库)。

支持的平台

MemorySanitizer 支持如下操作系统:

  • Linux
  • NetBSD
  • FreeBSD

限制

  • MemorySanitizer 会多消耗2x的实际内存,使用 origin tracking 的时候可能多达3x。
  • 在64位平台上,MemorySanitizer映射到(未来不一定)16+ Terabytes的虚拟地址空间. 这意味着一些工具如 ulimit 可能就不像通常那样生效了。
  • 不支持静态编译。
  • 旧版本的 MSan(LLVM 3.7以前版本)不支持非地址独立(non-position-independent)的可执行文件,并且在禁用 ASLR 的Linux内核版本中可能失败。更多信息请参考旧版本的文档。
  • FreeBSD 13中,MemorySanitizer 可能与地址独立(position-independent)的二进制不兼容,但是依然会对测试场景进行检测,在运行结束后会抛出一个警告。

当前状态

MemorySanitizer 可以用于非常庞大的、由源码重新编译的程序(如 Clang/LLVM 自身),以及所有依赖的库。

更多信息