CMake 秘籍(七)(4)https://developer.aliyun.com/article/1525427
工作原理
在本菜谱中,我们成功地将内存错误报告到了仪表板的动态分析部分。我们可以通过浏览缺陷(在缺陷计数下)获得更深入的见解:
通过点击各个链接,可以浏览完整输出。
请注意,也可以在本地生成 AddressSanitizer 报告。在本例中,我们需要设置ENABLE_ASAN
,如下所示:
$ mkdir -p build $ cd build $ cmake -DENABLE_ASAN=ON .. $ cmake --build . $ cmake --build . --target test Start 1: leaky 1/2 Test #1: leaky ............................***Failed 0.07 sec Start 2: use_after_free 2/2 Test #2: use_after_free ...................***Failed 0.04 sec 0% tests passed, 2 tests failed out of 2
直接运行leaky
测试可执行文件产生以下结果:
$ ./build/tests/leaky ================================================================= ==18536==ERROR: LeakSanitizer: detected memory leaks Direct leak of 8000 byte(s) in 1 object(s) allocated from: #0 0x7ff984da1669 in operator new[](unsigned long) /build/gcc/src/gcc/libsanitizer/asan/asan_new_delete.cc:82 #1 0x564925c93fd2 in function_leaky() /home/user/cmake-recipes/chapter-14/recipe-03/cxx-example/src/buggy.cpp:7 #2 0x564925c93fb2 in main /home/user/cmake-recipes/chapter-14/recipe-03/cxx-example/tests/leaky.cpp:4 #3 0x7ff98403df49 in __libc_start_main (/usr/lib/libc.so.6+0x20f49) SUMMARY: AddressSanitizer: 8000 byte(s) leaked in 1 allocation(s).
相应地,我们可以通过直接运行use_after_free
可执行文件来获得详细的输出,如下所示:
$ ./build/tests/use_after_free ================================================================= ==18571==ERROR: AddressSanitizer: heap-use-after-free on address 0x6250000004d8 at pc 0x557ffa8b0102 bp 0x7ffe8c560200 sp 0x7ffe8c5601f0 READ of size 8 at 0x6250000004d8 thread T0 #0 0x557ffa8b0101 in function_use_after_free() /home/user/cmake-recipes/chapter-14/recipe-03/cxx-example/src/buggy.cpp:28 #1 0x557ffa8affb2 in main /home/user/cmake-recipes/chapter-14/recipe-03/cxx-example/tests/use_after_free.cpp:4 #2 0x7ff1d6088f49 in __libc_start_main (/usr/lib/libc.so.6+0x20f49) #3 0x557ffa8afec9 in _start (/home/user/cmake-recipes/chapter-14/recipe-03/cxx-example/build/tests/use_after_free+0xec9) 0x6250000004d8 is located 984 bytes inside of 8000-byte region 0x625000000100,0x625000002040) freed by thread T0 here: #0 0x7ff1d6ded5a9 in operator delete[ /build/gcc/src/gcc/libsanitizer/asan/asan_new_delete.cc:128 #1 0x557ffa8afffa in function_use_after_free() /home/user/cmake-recipes/chapter-14/recipe-03/cxx-example/src/buggy.cpp:24 #2 0x557ffa8affb2 in main /home/user/cmake-recipes/chapter-14/recipe-03/cxx-example/tests/use_after_free.cpp:4 #3 0x7ff1d6088f49 in __libc_start_main (/usr/lib/libc.so.6+0x20f49) previously allocated by thread T0 here: #0 0x7ff1d6dec669 in operator new[](unsigned long) /build/gcc/src/gcc/libsanitizer/asan/asan_new_delete.cc:82 #1 0x557ffa8affea in function_use_after_free() /home/user/cmake-recipes/chapter-14/recipe-03/cxx-example/src/buggy.cpp:19 #2 0x557ffa8affb2 in main /home/user/cmake-recipes/chapter-14/recipe-03/cxx-example/tests/use_after_free.cpp:4 #3 0x7ff1d6088f49 in __libc_start_main (/usr/lib/libc.so.6+0x20f49) SUMMARY: AddressSanitizer: heap-use-after-free /home/user/cmake-recipes/chapter-14/recipe-03/cxx-example/src/buggy.cpp:28 in function_use_after_free() Shadow bytes around the buggy address: 0x0c4a7fff8040: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 0x0c4a7fff8050: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 0x0c4a7fff8060: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 0x0c4a7fff8070: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 0x0c4a7fff8080: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd =>0x0c4a7fff8090: fd fd fd fd fd fd fd fd fd fd fd[fd]fd fd fd fd 0x0c4a7fff80a0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 0x0c4a7fff80b0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 0x0c4a7fff80c0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 0x0c4a7fff80d0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 0x0c4a7fff80e0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb ==18571==ABORTING
如果我们不使用 AddressSanitizer 进行测试(默认情况下ENABLE_ASAN
为OFF
),则以下示例不会报告任何错误:
$ mkdir -p build_no_asan $ cd build_no_asan $ cmake .. $ cmake --build . $ cmake --build . --target test Start 1: leaky 1/2 Test #1: leaky ............................ Passed 0.00 sec Start 2: use_after_free 2/2 Test #2: use_after_free ................... Passed 0.00 sec 100% tests passed, 0 tests failed out of 2
确实,leaky
只会浪费内存,而use_after_free
可能导致非确定性失败。调试这些失败的一种方法是使用 valgrind(valgrind.org
)。
与前两个方案不同,我们使用了一个 CTest 脚本来配置、构建和测试代码,并将报告提交到仪表板。要了解这个方案的工作原理,请仔细查看dashboard.cmake
脚本。首先,我们定义项目名称并设置主机报告和构建名称,如下所示:
set(CTEST_PROJECT_NAME "example") cmake_host_system_information(RESULT _site QUERY HOSTNAME) set(CTEST_SITE ${_site}) set(CTEST_BUILD_NAME "${CMAKE_SYSTEM_NAME}-${CMAKE_HOST_SYSTEM_PROCESSOR}")
在我们的例子中,CTEST_BUILD_NAME
评估为Linux-x86_64
。在您的例子中,您可能会观察到不同的结果,这取决于您的操作系统。
接下来,我们为源代码和构建目录指定路径:
set(CTEST_SOURCE_DIRECTORY "${CTEST_SCRIPT_DIRECTORY}") set(CTEST_BINARY_DIRECTORY "${CTEST_SCRIPT_DIRECTORY}/build")
我们可以将生成器设置为Unix Makefiles
:
set(CTEST_CMAKE_GENERATOR "Unix Makefiles")
然而,为了编写更便携的测试脚本,我们更倾向于通过命令行提供生成器,如下所示:
$ ctest -S dashboard.cmake -D CTEST_CMAKE_GENERATOR="Unix Makefiles"
dashboard.cmake
中的下一个代码片段计算出机器上可用的核心数,并将测试步骤的并行级别设置为可用核心数,以最小化总测试时间:
include(ProcessorCount) ProcessorCount(N) if(NOT N EQUAL 0) set(CTEST_BUILD_FLAGS -j${N}) set(ctest_test_args ${ctest_test_args} PARALLEL_LEVEL ${N}) endif()
接下来,我们开始测试步骤并配置代码,设置ENABLE_ASAN
为ON
:
ctest_start(Experimental) ctest_configure( OPTIONS -DENABLE_ASAN:BOOL=ON )
剩余的dashboard.cmake
中的命令对应于构建、测试、内存检查和提交步骤:
ctest_build() ctest_test() set(CTEST_MEMORYCHECK_TYPE "AddressSanitizer") ctest_memcheck() ctest_submit()
还有更多
细心的读者会注意到,我们在链接目标之前并没有在我们的系统上搜索 AddressSanitizer。在现实世界的完整用例中,这样做是为了避免在链接阶段出现不愉快的意外。我们将提醒读者,我们在第 7 个方案中展示了一种方法来探测 sanitizers 的可用性,即“探测编译器标志”,在第五章“配置时间和构建时间操作”中。
更多关于 AddressSanitizer 的文档和示例,请参见github.com/google/sanitizers/wiki/AddressSanitizer
。AddressSanitizer 不仅限于 C 和 C++。对于 Fortran 示例,我们建议读者参考位于github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-14/recipe-03/fortran-example
的代码仓库。
在github.com/arsenm/sanitizers-cmake
上可以找到用于发现 sanitizers 并调整编译器标志的 CMake 工具。
另请参阅
以下博客文章讨论了如何添加对动态分析工具的支持的示例,并启发了当前的方案:blog.kitware.com/ctest-cdash-add-support-for-new-dynamic-analysis-tools/
。
使用 ThreadSanitizer 并将数据竞争报告给 CDash
本食谱的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-14/recipe-04
找到,并包含一个 C++示例。该食谱适用于 CMake 版本 3.5(及以上),并在 GNU/Linux 和 macOS 上进行了测试。
在本食谱中,我们将重用前一个示例的方法,但结合使用 ThreadSanitizer(或 TSan)与 CTest 和 CDash,以识别数据竞争并将这些信息报告给 CDash 仪表板。ThreadSanitizer 的文档可以在网上找到,网址为github.com/google/sanitizers/wiki/ThreadSanitizerCppManual
。
准备就绪
在本食谱中,我们将使用以下示例代码(example.cpp
):
#include <chrono> #include <iostream> #include <thread> static const int num_threads = 16; void increase(int i, int &s) { std::this_thread::sleep_for(std::chrono::seconds(1)); std::cout << "thread " << i << " increases " << s++ << std::endl; } int main() { std::thread t[num_threads]; int s = 0; // start threads for (auto i = 0; i < num_threads; i++) { t[i] = std::thread(increase, i, std::ref(s)); } // join threads with main thread for (auto i = 0; i < num_threads; i++) { t[i].join(); } std::cout << "final s: " << s << std::endl; return 0; }
在这个示例代码中,我们启动了 16 个线程,每个线程都调用了increase
函数。increase
函数休眠一秒钟,然后打印并递增一个整数s
。我们预计这段代码会表现出数据竞争,因为所有线程都在没有明确同步或协调的情况下读取和修改同一地址。换句话说,我们预计最终的s
,即代码末尾打印的s
,可能会在每次运行中有所不同。这段代码存在缺陷,我们将尝试借助 ThreadSanitizer 来识别数据竞争。如果不运行 ThreadSanitizer,我们可能不会发现代码中的任何问题:
$ ./example thread thread 0 increases 01 increases 1 thread 9 increases 2 thread 4 increases 3 thread 10 increases 4 thread 2 increases 5 thread 3 increases 6 thread 13 increases 7 thread thread 7 increases 8 thread 14 increases 9 thread 8 increases 10 thread 12 increases 11 thread 15 increases 12 thread 11 increases 13
5 increases 14 thread 6 increases 15 final s: 16
如何操作
让我们详细地逐一介绍必要的步骤:
CMakeLists.txt
文件首先定义了最低支持版本、项目名称、支持的语言,以及在这种情况下,对 C++11 标准的要求:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(recipe-04 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON)
- 接下来,我们定位 Threads 库,定义可执行文件,并将其与 Threads 库链接:
find_package(Threads REQUIRED) add_executable(example example.cpp) target_link_libraries(example PUBLIC Threads::Threads )
- 然后,我们提供选项和代码以支持 ThreadSanitizer 的编译和链接:
option(ENABLE_TSAN "Enable ThreadSanitizer" OFF) if(ENABLE_TSAN) if(CMAKE_CXX_COMPILER_ID MATCHES GNU) message(STATUS "ThreadSanitizer enabled") target_compile_options(example PUBLIC -g -O1 -fsanitize=thread -fno-omit-frame-pointer -fPIC ) target_link_libraries(example PUBLIC tsan ) else() message(WARNING "ThreadSanitizer not supported for this compiler") endif() endif()
- 最后,作为测试,我们执行编译后的示例本身:
enable_testing() # allow to report to a cdash dashboard include(CTest) add_test( NAME example COMMAND $<TARGET_FILE:example> )
CTestConfig.cmake
文件与前一个食谱相比没有变化:
set(CTEST_DROP_METHOD "http") set(CTEST_DROP_SITE "my.cdash.org") set(CTEST_DROP_LOCATION "/submit.php?project=cmake-cookbook") set(CTEST_DROP_SITE_CDASH TRUE)
- 相应的
dashboard.cmake
脚本是对前一个食谱的简单改编,以适应 TSan:
set(CTEST_PROJECT_NAME "example") cmake_host_system_information(RESULT _site QUERY HOSTNAME) set(CTEST_SITE ${_site}) set(CTEST_BUILD_NAME "${CMAKE_SYSTEM_NAME}-${CMAKE_HOST_SYSTEM_PROCESSOR}") set(CTEST_SOURCE_DIRECTORY "${CTEST_SCRIPT_DIRECTORY}") set(CTEST_BINARY_DIRECTORY "${CTEST_SCRIPT_DIRECTORY}/build") include(ProcessorCount) ProcessorCount(N) if(NOT N EQUAL 0) set(CTEST_BUILD_FLAGS -j${N}) set(ctest_test_args ${ctest_test_args} PARALLEL_LEVEL ${N}) endif() ctest_start(Experimental) ctest_configure( OPTIONS -DENABLE_TSAN:BOOL=ON ) ctest_build() ctest_test() set(CTEST_MEMORYCHECK_TYPE "ThreadSanitizer") ctest_memcheck() ctest_submit()
- 让我们再次为这个示例设置生成器,通过传递
CTEST_CMAKE_GENERATOR
选项:
$ ctest -S dashboard.cmake -D CTEST_CMAKE_GENERATOR="Unix Makefiles" Each . represents 1024 bytes of output . Size of output: 0K Each symbol represents 1024 bytes of output. '!' represents an error and '*' a warning. . Size of output: 0K
- 在仪表板上,我们将看到以下内容:
- 我们可以更详细地看到动态分析如下:
它是如何工作的
本食谱的核心成分位于以下部分的CMakeLists.txt
中:
option(ENABLE_TSAN "Enable ThreadSanitizer" OFF) if(ENABLE_TSAN) if(CMAKE_CXX_COMPILER_ID MATCHES GNU) message(STATUS "ThreadSanitizer enabled") target_compile_options(example PUBLIC -g -O1 -fsanitize=thread -fno-omit-frame-pointer -fPIC ) target_link_libraries(example PUBLIC tsan ) else() message(WARNING "ThreadSanitizer not supported for this compiler") endif() endif()
成分也包含在dashboard.cmake
中更新的步骤中:
# ... ctest_start(Experimental) ctest_configure( OPTIONS -DENABLE_TSAN:BOOL=ON ) ctest_build() ctest_test() set(CTEST_MEMORYCHECK_TYPE "ThreadSanitizer") ctest_memcheck() ctest_submit()
与前一个食谱一样,我们也可以在本地检查 ThreadSanitizer 的输出:
$ mkdir -p build $ cd build $ cmake -DENABLE_TSAN=ON .. $ cmake --build . $ cmake --build . --target test Start 1: example 1/1 Test #1: example ..........................***Failed 1.07 sec 0% tests passed, 1 tests failed out of 1 $ ./build/example thread 0 increases 0 ================== WARNING: ThreadSanitizer: data race (pid=24563) ... lots of output ... SUMMARY: ThreadSanitizer: data race /home/user/cmake-recipes/chapter-14/recipe-04/cxx-example/example.cpp:9 in increase(int, int&)
还有更多内容
对 OpenMP 代码应用 TSan 是一个自然的步骤,但请注意,在某些情况下,OpenMP 在 TSan 下会产生误报。对于 Clang 编译器,一个解决办法是重新编译编译器本身及其libomp
,并使用-DLIBOMP_TSAN_SUPPORT=TRUE
。通常,合理地使用检测器可能需要重新编译整个工具栈,以避免误报。对于使用 pybind11 的 C++项目,我们可能需要重新编译启用了检测器的 Python,以获得有意义的结果。或者,可以通过使用检测器抑制来将 Python 绑定排除在检测之外,如github.com/google/sanitizers/wiki/ThreadSanitizerSuppressions
所述。如果例如一个共享库被一个启用了检测的二进制文件和一个 Python 插件同时调用,这可能是不可能的。
另请参阅
以下博客文章讨论了如何为动态分析工具添加支持的示例,并激发了当前的方案:blog.kitware.com/ctest-cdash-add-support-for-new-dynamic-analysis-tools/
。