新特性出炉
近日,一个新的特性被添加到了MSVC工具集中,这个就是Windows C++开发者梦寐以求的AddressSanitizer(ASan)。为什么这么说:GCC早在4.8就可以支持ASan了,是时候轮到咱大Windows开发者了吧?
啥是ASan?
ASan是一个用来快速检测内存错误的工具,它可以找到程序中出现的内存访问违规问题,例如使用已经释放了的内存,内存指针越界访问等。一直以来,ASan支持一直都是开发者社区中呼声较高的一项特性,今天,我们可以说,我们可以在Windows开发平台中使用上ASan了。
在微软,我们相信:我们的开发者应该能根据自己的需要来选择不同的开发工具,我们也非常高兴和LLVM团队合作,将他们的一些十分好用的工具集成到Visual Studio中。之前,我们也引入了一些工具特性,例如clang-format,以及最近更新的clang-tidy。MSVC中对ASan的支持将会在我们的第二个VS2019 v16.4预览版中和大家见面。
为了在Windows和MSVC中支持ASan,我们做了以下的工作:
=> 更新了ASan运行时库,使其能更加好的在Windows平台工作
=> MSVC编译器可以使用ASan进行二进制Profiling
=> CMake和MSBuild也进行了更新来支持ASan
=> VS调试器可以在Windows二进制中检测到ASan错误
=> ASan可以在VS安装程序中的C++ Develop workload中被安装
当你调试一个已经启用了ASan的程序时,Visual Studio中的Exception Helper可以显示一个内存错误,并退出程序运行。同时,你可以可以在输出窗口看到有关的错误信息。
在Windows中安装ASan
在默认情况下,ASan已经包含在了C++ Desktop workload中,注意这个仅在全新安装时才会可见。如果你是从VS2019的一个旧版本上升级,则需要在升级完成之后来启用ASan支持。如下图所示:
在上图中,你可以从VS安装程序中修改已经安装的工具。请注意,如果你运行的是更新版本的Visual Studio但是还未启用ASan,则当你运行程序时,将会出现如下的错误:
LNK 1356 – cannot find library ‘clang_rt.asan_dynamic-i386.lib’
为Windows MSBuild工程启用ASan
在MSBuild工程中,你可以通过点击工程属性下面的C/C++ > 常规,启用[Enable Address Sanitizer(Experimental)]来启用ASan。同时,这个方法也适用于MSBuild Linux工程。
请注意:当前ASan只能在x86二进制下工作,在将来的版本中,会支持更多的架构。
为Windows CMake工程启用ASan
为了在目标平台为Windows的CMake工程启用ASan,可以按照如下的方法:
1. 在IDE中打开配置下拉列表,点击[Manage Configurations],这回打开CMake工程的配置界面,在这个配置界面的修改会保存到CMakeSettings.json文件中。
2. 点击[Edit JSON]链接,这将会切换到原始版本的json文本内容。
3. 在[x86-Release]配置节点下,添加属性:”addressSanitizerEnabled”: true
你会看到会出现一个警告信息标识:Property name is not allowed by the schema.
但是请不用担心,这个是一个小Bug,会很快被修复。
下面是一个修改之后的CMakeSettings.json文件的部分截图:
在将来的版本更新中,我们将想办法进一步的简化在CMake工程中启用ASan的步骤,敬请期待。
ASan运行时开发
为了增强ASan在Windows平台上的体验,我们决定为LLVM compiler-rt 工程贡献代码,并重用这个工程中的ASan运行时库的实现。我们对这个工程的贡献包括一些BugFix和优化的HeapAlloc , RtlAllocateHeap , GlobalAlloc , 和LocalAlloc拦截代码,并使用了他们的Free , ReAllocate , 和Size等函数实现。开发者可以通过添加ASAN_OPTIONS环境变量来在Clang工具集或者MSVC工具集中启用这些特性,如下图所示:
为支持ASan,MSVC中做了如下的修改
为了支持ASan这一特性,我们修改了c1.dll和c2.dll,使其在编译期在目标程序中加入instrumentaion代码。对于一个32位的地址空间,大约会有200MB的内存被分配来隐射整地址空间。当一个内存被分配,在地址空间中有个标志位会被修改来表示这个内存分配有效并可访问。当这个内存被释放或者变量离开作用域时,地址空间里对应的标志位会被修改,来映射这个内存分配变得不再有效。
潜在危险的内存访问会在地址空间中被重复检测来验证运行时访问时的有效性。当发生内存访问违规时,错误信息会被输出至stderr或者以一个异常的方式来报告给开发者。在地址空间中,当访问一个内存区域时,其内存数据会首先被检查。ASan的算法可以报告内存访问错误的具体位置以及错误的原因。
这意味着,如果一个程序被MSVC+ASan编译,则这个程序会添加一个对clang_rt.asan的依赖。每个库都有特殊的使用场景,如果你的程序比较复杂则对应的链接过程也会比较复杂。
从控制台中编译ASan
在控制台中,你可以手动的链接ASan库。对于x86平台二进制文件,有如下的库需要被链接:
=> clang_rt.asan-i386.lib – static runtime compatible with /MT CRT.
=> clang_rt.asan_cxx-i386.lib -static runtime component which adds support for new and delete, also compatible with /MT CRT.
=> clang_rt.asan_dynamic-i386.lib – dynamic import library, compatible with /MD CRT.
=> clang_rt.asan_dynamic-i386.dll – dynamic runtime DLL, compatible with /MD.
=> clang_rt.asan_dynamic_runtime_thunk-i386.lib – dynamic library to import and intercept some /MD CRT functions manually.
=> clang_rt.asan_dll_thunk-i386.lib – import library which allows an ASAN instrumented DLL to use the static ASan library which is linked into the main executable. Compatible with /MT CRT.
当你选择了合适的ASan运行时库,则应添加命令行/wholearchive:到链接参数中并添加指定的库文件。clang_rt.asan_dynamic_i386.dll这个文件将不会安装到System32目录,因此当运行你的程序的时候,需要确保ASan运行时库可以被加载器找到。
以下是一些额外的建议
=> 编译单个静态执行文件: 链接静态运行库(asan-i386.lib)和对应的cxx库。
=> 以/MT运行时库链接可执行文件同时使用ASan DLL: 可执行文件将链接asan-i386.lib并且DLL需要clang_rt.asan_dll_thunk-i386.lib。这可以使DLL使用运行时链接到主程序中并且避免潜在的Shadow memory的冲突问题。
=> 以/MD进行动态编译:所有的EXE和DLL文件必须链接asan_dynamic-i386.lib和clang_rt.asan_dynamic_runtime_thunk-i386.lib。在运行时,这些库文件将会引用对应的clang_rt.asan_dynamic-i386.dll文件。
另外请注意:ASan运行时库将会在执行期对内存管理函数进行拦截并将函数调用重定向至ASan的对应的封装函数。当运行时环境和库编写时的目标环境不一样的时候,这可能会造成一些不稳定的问题。
总结
通过使用ASan,可以有效的检测一些潜在的内存访问问题,真是我们C++程序开发者的福音啊。