AddressSanitizer 运行时
AddressSanitizer 运行时库拦截常见的内存分配函数和操作,以检查内存访问。 有几个不同的运行时库可支持编译器可能生成的各种类型的可执行文件。 只要在编译时传递 /fsanitize=address
选项,编译器和链接器就会自动链接相应的运行时库。 可以在链接时使用 /NODEFAULTLIB
选项重写默认行为。 有关详细信息,请参阅 AddressSanitizer 语言、生成和调试参考中有关链接的部分。
使用 cl /fsanitize=address
进行编译时,编译器会生成指令来管理和检查影子字节。 程序使用此检测来检查堆栈、堆或全局范围内的内存访问。 编译器还会生成描述堆栈和全局变量的元数据。 此元数据使运行时能够生成精确的错误诊断:源代码中的函数名称、行和列。 将编译器检查和运行时库相结合,可以在运行时遇到多种类型的内存安全 bug 时进行准确诊断。
从 Visual Studio 17.7 预览版 3 开始,用于链接到 AddressSanitizer 运行时的运行时库列表如下所示。 有关 /MT
(静态链接运行时)和 /MD
(在运行时动态链接 redist)选项的详细信息,请参阅 /MD、/MT、/LD(使用运行时库)。
注意
在下表中,{arch}
是 i386
或 x86_64
。
这些库使用体系结构名称的 Clang 约定。 MSVC 约定通常是 x86
和 x64
,而不是 i386
和 x86_64
,但它们引用相同的体系结构。
CRT 选项 | AddressSanitizer 运行时库 (.lib) | 地址运行时二进制文件 (.dll) |
---|---|---|
/MT 或 /MTd |
%> | clang_rt.asan_dynamic-{arch} |
/MD 或 /MDd |
%> | clang_rt.asan_dynamic-{arch} |
下图显示 /MT
、/MTd
、/MD
和 /MDd
编译器选项下如何链接语言运行时库:
图片显示了三种链接运行时库的方案。 第一种是 /MT 或 /MTd。 My_exe.exe 和 my_dll.dll 都以自己的静态链接 VCRuntime、通用 CRT 和 C++ 运行时的副本显示。 方案显示在 /MD 中 my_exe.exe 和 my_dll.dll 共享 vcruntime140.dll、ucrtbase.dll 和 msvcp140.dll。 最后一种方案显示在 /MDd 中 my_exe.exe 和 my_dll.dll 共享运行时的调试版本:vcruntime140d.dll、ucrtbased.dll 和 msvcp140d.dll
下图显示了不同编译器选项下如何链接 ASan 库:
图片显示了四种链接 ASan 运行时库的方案。 方案适用于 /MT(静态链接运行时)、/MTd(静态链接调试运行时)、/MD(在运行时动态链接 redist)、/MDd(在运行时动态链接调试 redist)。 在所有情况下,my_exe.exe 及其关联 my_dll.dll 都链接到单个 clang-rt.asan-dynamix-x86_64.dll 实例。
与其他 C 运行时组件不同,即使是静态链接,ASan 运行时 DLL 也必须出现在运行时中。
以前的版本
在 Visual Studio 17.7 预览版 3 之前,静态链接(/MT
或 /MTd
)的版本不使用 DLL 依赖项。 与之相反,AddressSanitizer 运行时是静态链接到用户的 EXE 中的。 然后,DLL 项目将从用户的 EXE 加载导出以访问 ASan 功能。
动态链接的项目(/MD
或 /MDd
)根据其配置目的为调试或发布而使用不同的库和 DLL。 有关这些更改及其动机的详细信息,请参阅 MSVC 地址擦除系统 – 一个 DLL 适用于所有运行时配置。
下表描述了 Visual Studio 17.7 预览版 3 之前 AddressSanitizer 运行时库链接的先前行为:
CRT 选项 | DLL 或 EXE | 调试? | ASan 库 (.lib ) |
ASan 运行时二进制 (.dll ) |
---|---|---|---|---|
/MT |
EXE | 否 | %> | 无 |
/MT |
DLL | 否 | clang_rt.asan_dll_thunk-{arch} |
无 |
/MD |
或 | 否 | %> | clang_rt.asan_dynamic-{arch} |
/MT |
EXE | 是 | %> | 无 |
/MT |
DLL | 是 | clang_rt.asan_dbg_dll_thunk-{arch} |
无 |
/MD |
任一个 | 是 | %> | clang_rt.asan_dbg_dynamic-{arch} |
下图显示了 Visual Studio 2022 17.7 预览版 3 之前,不同编译器选项下如何链接 ASan 库:
图片显示了四种链接 ASan 运行时库的方案。 方案适用于 /MT(静态链接运行时)、/MTd(静态链接调试运行时)、/MD(在运行时动态链接 redist)、/MDd(在运行时动态链接调试 redist)。 对于 /MT,my_exe.exe 具有 ASan 运行时的静态链接副本。 my_dll.dll 链接 my_exe.exe 中的 ASan 运行时。 /MTd 的示意图相同,但是它使用静态链接调试 ASan 运行时。 对于 /MD,my_exe.exe 和 my_dll.dll 都链接到名为 clang_rt.asan_dynamic-x86_64.dll 的动态链接 ASan 运行时上。 /MDd 的示意图相同,但是 my_exe.exe 和 my_dll.dll 链接名为 clang_rt.asan_dbg_dynamic-x86_64.dll 的调试 ASan 运行时。
函数拦截
AddressSanitizer 通过多种热修补技术实现函数拦截。 源代码本身对这些技术进行了最好的记录。
运行时库拦截许多常见的内存管理和内存操作函数。 有关列表,请参阅 AddressSanitizer 拦截函数列表。 分配拦截器管理与每个分配调用相关的元数据和影子字节。 每次调用 CRT 函数(例如 malloc
或 delete
)时,拦截器都会在 AddressSanitizer 影子内存区域中设置特定值,以指示这些堆位置当前是否可访问以及分配的边界是什么。 这些影子字节允许编译器生成对影子字节的检查,以确定负载或存储是否有效。
无法保证拦截成功。 如果函数序言太短而无法写入 jmp
,则拦截可能会失败。 如果拦截失败,程序将引发 debugbreak
并停止。 如果附加一个调试器,则可以清楚了解拦截问题的原因。 如果遇到此问题,请报告 bug。
注意
用户可以选择尝试在拦截失败后继续执行,方法是将环境变量 ASAN_WIN_CONTINUE_ON_INTERCEPTION_FAILURE
设置为任意值。 在拦截失败后继续执行可能会导致丢失该函数的 bug 报告。
自定义分配器和 AddressSanitizer 运行时
AddressSanitizer 运行时为常见的分配器接口 malloc
/free
、new
/delete
、HeapAlloc
/HeapFree
(通过 RtlAllocateHeap
/RtlFreeHeap
)提供拦截器。 许多程序会出于某种原因使用自定义分配器,例如使用 dlmalloc
的任何程序或使用 std::allocator
接口和 VirtualAlloc()
的解决方案。 编译器无法自动将影子内存管理调用添加到自定义分配器。 用户有责任使用提供的手动中毒接口。 此 API 使这些分配器能够与现有的 AddressSanitizer 运行时和影子字节约定一起正常运行。
手动 AddressSanitizer 中毒接口
启蒙的接口很简单,但它对用户施加了对齐限制。 用户可以通过导入 sanitizer/asan_interface.h
来导入这些原型。 以下是接口函数原型:
void __asan_poison_memory_region(void const volatile *addr, size_t size);
void __asan_unpoison_memory_region(void const volatile *addr, size_t size);
为方便起见,AddressSanitizer 接口头文件提供了包装器宏。 这些宏检查在编译期间是否启用了 AddressSanitizer 功能。 它们允许源代码在不需要时省略中毒函数调用。 这些宏应该优先于直接调用上述函数:
#define ASAN_POISON_MEMORY_REGION(addr, size)
#define ASAN_UNPOISON_MEMORY_REGION(addr, size)
AddressSanitizer 中毒的对齐要求
影子字节的任何手动中毒都必须考虑对齐要求。 如有必要,用户必须添加填充,以便影子字节在影子内存中的字节边界处结束。 AddressSanitizer 影子内存中的每个位对应用程序内存中单个字节的状态进行编码。 这种编码意味着每个分配(包括任何填充)的总大小必须与 8 字节边界对齐。 如果不满足对齐要求,可能会生成 bug 报告。 不正确的报告可能表现为缺少报告(漏报)或非错误报告(误报)。
有关对齐要求和潜在问题的说明,请参阅提供的 ASan 对齐示例。 一个是一个小程序,显示手动影子内存中毒可能会出错的情况。 第二个是使用 std::allocator
接口的手动中毒的示例实现。
运行时选项
Microsoft C/C++ (MSVC) 使用基于 llvm 项目存储库中的 Clang AddressSanitizer 运行时的运行时。 因此,大多数运行时选项在两个版本之间共享。 此处提供了公共 Clang 运行时选项的完整列表。 以下各部分介绍了其中一些差异。 如果发现某选项无法按预期运行,请报告 bug。
不支持的 AddressSanitizer 选项
- detect_container_overflow
- unmap_shadow_on_exit
注意
AddressSanitizer 运行时选项 halt_on_error
无法按预期方式运行。 在 Clang 和 MSVC 运行时库中,许多错误类型被视为不可持续,包括大多数内存损坏错误。
有关详细信息,请参阅与 Clang 12.0 的差异部分。
特定于 MSVC 的 AddressSanitizer 运行时选项
windows_hook_legacy_allocators
布尔值,设置为false
以禁用对GlobalAlloc
和LocalAlloc
分配器的拦截。注意
撰写本文时,公共 llvm 项目运行时中没有
windows_hook_legacy_allocators
选项。 该选项最终可能会进入公共项目;但需要经过代码审查和社区接受。选项
windows_hook_rtl_allocators
以前是一个可选功能,因为 AddressSanitizer 是试验性的,但现在默认启用。 在 Visual Studio 2022 版本 17.4.6 之前的版本中,默认选项值为false
。 在 Visual Studio 2022 版本 17.4.6 及更高版本中,选项windows_hook_rtl_allocators
默认为true
。iat_overwrite
字符串,默认设置为"error"
。 其他可取的值为"protect"
和"ignore"
。 某些模块可能会覆盖其他模块的import address table
以自定义某些函数的实现。 例如,驱动程序通常为特定硬件提供自定义实现。 该iat_overwrite
选项管理 AddressSanitizer 运行时对特定memoryapi.h
函数的防覆盖保护。 运行时当前跟踪VirtualAlloc
、VirtualProtect
和VirtualQuery
函数以进行保护。 Visual Studio 2022 版本 17.5 预览版 1 及更高版本提供该选项。 以下iat_overwrite
值控制在覆盖受保护函数时运行时的反应方式:- 如果设置为
"error"
(默认值),运行时将在检测到覆盖时报告错误。 - 如果设置为
"protect"
,运行时将尝试避免使用被覆盖的定义并继续操作。 实际上,该函数的原始memoryapi
定义在运行时内部使用,以避免无限递归。 进程中的其他模块仍使用被覆盖的定义。 - 如果设置为
"ignore"
,运行时不会尝试更正任何被覆盖的函数,并继续执行。
- 如果设置为
windows_fast_fail_on_error
布尔值(默认情况下为 false),设置为true
,使进程在打印错误报告后以 __fastfail(71) 终止。
注意
当 abort_on_error 值设置为 true 时,在 Windows 上,程序以 exit(3) 终止。 为了不更改当前行为,我们决定改为引入这一新选项。 如果 abort_on_error 和 windows_fast_fail_on_error 均为 true,则程序将以 __fastfail 退出。
AddressSanitizer 拦截函数列表 (Windows)
AddressSanitizer 运行时热修补许多函数,以在运行时启用内存安全检查。 下面是 AddressSanitizer 运行时监视的函数的非详尽列表。
默认拦截器
__C_specific_handler
(仅限 x64)_aligned_free
_aligned_malloc
_aligned_msize
_aligned_realloc
_calloc_base
_calloc_crt
_calloc_dbg
(仅限调试运行时)_except_handler3
(仅限 x86)_except_handler4
(仅限 x86)(未记录)_expand
_expand_base
(未记录)_expand_dbg
(仅限调试运行时)_free_base
(未记录)_free_dbg
(仅限调试运行时)_malloc_base
(未记录)_malloc_crt
(未记录)_malloc_dbg
(仅限调试运行时)_msize
_msize_base
(未记录)_msize_dbg
(仅限调试运行时)_realloc_base
(未记录)_realloc_crt
(未记录)_realloc_dbg
(仅限调试运行时)_recalloc
_recalloc_base
(未记录)_recalloc_crt
(未记录)_recalloc_dbg
(仅限调试运行时)_strdup
atoi
atol
calloc
CreateThread
free
frexp
longjmp
malloc
memchr
memcmp
memcpy
memmove
memset
RaiseException
realloc
RtlAllocateHeap
RtlCreateHeap
RtlDestroyHeap
RtlFreeHeap
RtlRaiseException
RtlReAllocateHeap
(未记录)RtlSizeHeap
(未记录)SetUnhandledExceptionFilter
strcat
strchr
strcmp
strcpy
strcspn
strdup
strlen
strncat
strncmp
strncpy
strnlen
strpbrk
strspn
strstr
strtok
strtol
wcslen
wcsnlen
可选拦截器
仅当启用了 AddressSanitizer 运行时选项时,才会安装此处列出的拦截器。 将 windows_hook_legacy_allocators
设置为 false
以禁用旧式分配器拦截。
set ASAN_OPTIONS=windows_hook_legacy_allocators=false
GlobalAlloc
GlobalFree
GlobalHandle
GlobalLock
GlobalReAlloc
GlobalSize
GlobalUnlock
LocalAlloc
LocalFree
LocalHandle
LocalLock
LocalReAlloc
LocalSize
LocalUnlock
另请参阅
AddressSanitizer 概述
AddressSanitizer 已知问题
AddressSanitizer 生成和语言参考
AddressSanitizer 阴影字节
AddressSanitizer 云或分布式测试
AddressSanitizer 调试程序集成
AddressSanitizer 错误示例