内存消毒器¶
简介¶
内存消毒器是一种未初始化内存使用检测器。它包含一个编译器检测模块和一个运行时库。
内存消毒器引入的典型减速为 **3x**。
以下是不完整的内存消毒器将报告错误的案例列表
在条件分支中使用了未初始化的值。
未初始化的指针用于内存访问。
未初始化的值被传递或从函数调用中返回,这被认为是未定义的行为。可以使用
-fno-sanitize-memory-param-retval
禁用检查。未初始化的数据被传递到一些 libc 调用中。
如何构建¶
使用 CMake 构建 LLVM/Clang。
用法¶
只需使用 -fsanitize=memory
标志编译和链接程序。内存消毒器运行时库应链接到最终的可执行文件,因此确保使用 clang
(而不是 ld
)进行最终链接步骤。当链接共享库时,不会链接内存消毒器运行时,因此 -Wl,-z,defs
可能导致链接错误(不要将其与内存消毒器一起使用)。要获得合理的性能,请添加 -O1
或更高版本。要获取错误消息中有意义的堆栈跟踪,请添加 -fno-omit-frame-pointer
。要获得完美的堆栈跟踪,您可能需要禁用内联(只需使用 -O1
)和尾调用消除 (-fno-optimize-sibling-calls
)。
% 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
如果检测到错误,程序将向 stderr 打印一条错误消息并以非零退出代码退出。
% ./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
默认情况下,内存消毒器在检测到第一个错误时退出。如果您发现错误报告难以理解,请尝试启用 起源跟踪。
__has_feature(memory_sanitizer)
¶
在某些情况下,可能需要根据内存消毒器是否启用执行不同的代码。 __has_feature 可用于此目的。
#if defined(__has_feature)
# if __has_feature(memory_sanitizer)
// code that builds only under MemorySanitizer
# endif
#endif
__attribute__((no_sanitize("memory")))
¶
某些代码不应由内存消毒器检查。可以使用函数属性 no_sanitize("memory")
来禁用特定函数中的未初始化检查。内存消毒器可能仍然会检测此类函数以避免误报。此属性可能不受其他编译器的支持,因此建议将其与 __has_feature(memory_sanitizer)
结合使用。
__attribute__((disable_sanitizer_instrumentation))
¶
可以使用 disable_sanitizer_instrumentation
属性应用于函数以防止所有类型的检测。结果,它可能会引入误报,因此应谨慎使用,并且仅在绝对必要时使用;例如,对于某些无法容忍任何检测和由此产生的副作用的代码。
忽略列表¶
内存消毒器支持 消毒器特殊情况列表 中的 src
和 fun
实体类型,可用于放宽特定源文件和函数的内存消毒器检查。所有“使用未初始化值”警告都将被抑制,并且从内存加载的所有值都将被视为完全初始化。
报告符号化¶
内存消毒器使用外部符号化器在报告中打印文件和行号。确保 llvm-symbolizer
二进制文件位于 PATH
中,或设置环境变量 MSAN_SYMBOLIZER_PATH
指向它。
起源跟踪¶
内存消毒器可以跟踪未初始化值的起源,类似于 Valgrind 的 –track-origins 选项。此功能由 -fsanitize-memory-track-origins=2
(或简写为 -fsanitize-memory-track-origins
)Clang 选项启用。使用上面示例中的代码,
% 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
默认情况下,内存消毒器会收集所有分配点和未初始化值经过的所有中间存储。起源跟踪已被证明对调试内存消毒器报告非常有用。它会将程序执行速度降低 1.5x-2x(在通常的内存消毒器减速之上)并增加内存开销。
Clang 选项 -fsanitize-memory-track-origins=1
启用了一种稍微更快的模式,其中内存消毒器仅收集分配点,而不是中间存储。
销毁后使用检测¶
内存消毒器包含销毁后使用检测。在调用析构函数后,该对象将被视为不再可读,并且使用底层内存将导致运行时错误报告。请参阅标准以获取 生命周期 定义。
此功能可以使用以下任一方法禁用
在编译期间传递额外的 Clang 选项
-fno-sanitize-memory-use-after-dtor
。在运行程序之前设置环境变量 MSAN_OPTIONS=poison_in_dtor=0。
处理外部代码¶
内存消毒器要求所有程序代码都经过检测。这也包括程序依赖的任何库,甚至 libc。未能做到这一点可能会导致误报。出于同样的原因,您可能需要将写入内存的所有内联汇编代码替换为纯 C/C++ 代码。
实现完整的内存消毒器检测非常困难。为了简化操作,内存消毒器运行时库包含 70 多个针对最常见 libc 函数的拦截器。它们使得可以使用未检测的 libc 链接运行内存消毒器检测的程序成为可能。例如,作者能够通过使用自建的检测 libc++(作为 libstdc++ 的替代)链接它来引导内存消毒器检测的 Clang 编译器。
安全注意事项¶
内存消毒器是一种错误检测工具,其运行时不适合与生产可执行文件链接。虽然它可能对测试有用,但内存消毒器的运行时并非旨在满足安全敏感的约束,并且可能会损害生成的执行文件的安全性。
支持的平台¶
内存消毒器在以下操作系统上受支持
Linux
NetBSD
FreeBSD
局限性¶
内存消毒器使用比本地运行多 2 倍的实际内存,使用起源跟踪则为 3 倍。
内存消毒器映射(但未保留)64 TB 的虚拟地址空间。这意味着像
ulimit
这样的工具可能无法像预期的那样正常工作。不支持静态链接。
旧版本的 MSan(LLVM 3.7 及更早版本)无法与非位置无关可执行文件一起使用,并且在某些禁用 ASLR 的 Linux 内核版本上可能会失败。有关更多详细信息,请参阅旧版本的文档。
内存消毒器可能与 FreeBSD 13 的位置无关可执行文件不兼容,但会在运行时进行检查并在这种情况下发出警告。
当前状态¶
内存消毒器已知可在大型真实世界程序(如 Clang/LLVM 本身)上运行,这些程序可以从源代码重新编译,包括所有依赖库。