14.3 使用AddressSanifier向CDash报告内存缺陷

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-14/recipe-03 中找到,其中包含一个C++示例和一个Fortran例子。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。

AddressSanitizer(ASan)是可用于C++、C和Fortran的内存检测。它可以发现内存缺陷,比如:在空闲后使用、返回后使用、作用域后使用、缓冲区溢出、初始化顺序错误和内存泄漏(请参见 https://github.com/google/sanitizers/wiki/AddressSanitizer )。从3.1版本开始,AddressSanitizer是LLVM的一部分;从4.8版本开始,作为GCC的一部分。在这个示例中,我们将在代码中加入两个bug,正常的测试中可能无法检测到。为了检测这些bug,我们将使用AddressSanitizer工具,并将CTest与动态分析结合起来,从而将缺陷报告给CDash。

准备工作

这个例子中,我们将使用两个源文件和两个测试集:

  1. .
  2. ├── CMakeLists.txt
  3. ├── CTestConfig.cmake
  4. ├── dashboard.cmake
  5. ├── src
  6. ├── buggy.cpp
  7. ├── buggy.hpp
  8. └── CMakeLists.txt
  9. └── tests
  10. ├── CMakeLists.txt
  11. ├── leaky.cpp
  12. └── use_after_free.cpp

buggy.cpp包含有两个bug:

  1. #include "buggy.hpp"
  2. #include <iostream>
  3. int function_leaky() {
  4. double *my_array = new double[1000];
  5. // do some work ...
  6. // we forget to deallocate the array
  7. // delete[] my_array;
  8. return 0;
  9. }
  10. int function_use_after_free() {
  11. double *another_array = new double[1000];
  12. // do some work ...
  13. // deallocate it, good!
  14. delete[] another_array;
  15. // however, we accidentally use the array
  16. // after it has been deallocated
  17. std::cout << "not sure what we get: " << another_array[123] << std::endl;
  18. return 0;
  19. }

这些函数在相应的头文件中声明(buggy.hpp):

  1. #pragma once
  2. int function_leaky();
  3. int function_use_after_free();

测试文件leaky.cpp中将会验证function_leaky的返回值:

  1. #include "buggy.hpp"
  2. int main() {
  3. int return_code = function_leaky();
  4. return return_code;
  5. }

相应地,use_after_free.cpp会检查function_use_after_free的返回值:

  1. #include "buggy.hpp"
  2. int main() {
  3. int return_code = function_use_after_free();
  4. return return_code;
  5. }

具体实施

为了使用ASan,我们需要使用特定的标志来编译代码。然后,我们将运行测试并将它们提交到面板。

  1. 生成bug库的工作将在src/CMakeLists.txt中完成:

    1. add_library(buggy "")
    2. target_sources(buggy
    3. PRIVATE
    4. buggy.cpp
    5. PUBLIC
    6. ${CMAKE_CURRENT_LIST_DIR}/buggy.hpp
    7. )
    8. target_include_directories(buggy
    9. PUBLIC
    10. ${CMAKE_CURRENT_LIST_DIR}
    11. )
  2. 在文件src/CMakeLists.txt中,我们将添加一个选项用于使用ASan:

    1. option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
    2. if(ENABLE_ASAN)
    3. if(CMAKE_CXX_COMPILER_ID MATCHES GNU)
    4. message(STATUS "AddressSanitizer enabled")
    5. target_compile_options(buggy
    6. PUBLIC
    7. -g -O1 -fsanitize=address -fno-omit-frame-pointer
    8. )
    9. target_link_libraries(buggy
    10. PUBLIC
    11. asan
    12. )
    13. else()
    14. message(WARNING "AddressSanitizer not supported for this compiler")
    15. endif()
    16. endif()
  3. 测试在tests/CMakeLists.txt中定义:

    1. foreach(_test IN ITEMS leaky use_after_free)
    2. add_executable(${_test} ${_test}.cpp)
    3. target_link_libraries(${_test} buggy)
    4. add_test(
    5. NAME
    6. ${_test}
    7. COMMAND
    8. $<TARGET_FILE:${_test}>
    9. )
    10. endforeach()
  4. CMakeLists.txt与之前的示例基本相同:

    1. # set minimum cmake version
    2. cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
    3. # project name and language
    4. project(recipe-03 LANGUAGES CXX)
    5. # require C++11
    6. set(CMAKE_CXX_STANDARD 11)
    7. set(CMAKE_CXX_EXTENSIONS OFF)
    8. set(CMAKE_CXX_STANDARD_REQUIRED ON)
    9. # process src/CMakeLists.txt
    10. add_subdirectory(src)
    11. enable_testing()
    12. # allow to report to a cdash dashboard
    13. include(CTest)
    14. # process tests/CMakeLists.txt
    15. add_subdirectory(tests)
  5. CTestConfig.cmake也没有修改:

    1. set(CTEST_DROP_METHOD "http")
    2. set(CTEST_DROP_SITE "my.cdash.org")
    3. set(CTEST_DROP_LOCATION "/submit.php?project=cmake-cookbook")
    4. set(CTEST_DROP_SITE_CDASH TRUE)
  6. 这个示例中,我们使用CTest脚本向CDash提交结果;为此,我们将创建一个文件dashboard.cmake(与主CMakeLists.txt` CTestConfig.cmake位于同一个目录下):

    1. set(CTEST_PROJECT_NAME "example")
    2. cmake_host_system_information(RESULT _site QUERY HOSTNAME)
    3. set(CTEST_SITE ${_site})
    4. set(CTEST_BUILD_NAME "${CMAKE_SYSTEM_NAME}-${CMAKE_HOST_SYSTEM_PROCESSOR}")
    5. set(CTEST_SOURCE_DIRECTORY "${CTEST_SCRIPT_DIRECTORY}")
    6. set(CTEST_BINARY_DIRECTORY "${CTEST_SCRIPT_DIRECTORY}/build")
    7. include(ProcessorCount)
    8. ProcessorCount(N)
    9. if(NOT N EQUAL 0)
    10. set(CTEST_BUILD_FLAGS -j${N})
    11. set(ctest_test_args ${ctest_test_args} PARALLEL_LEVEL ${N})
    12. endif()
    13. ctest_start(Experimental)
    14. ctest_configure(
    15. OPTIONS
    16. -DENABLE_ASAN:BOOL=ON
    17. )
    18. ctest_build()
    19. ctest_test()
    20. set(CTEST_MEMORYCHECK_TYPE "AddressSanitizer")
    21. ctest_memcheck()
    22. ctest_submit()
  7. 我们将执行dashboard.cmake脚本。注意,我们使用CTEST_CMAKE_GENERATOR与生成器选项的方式:

    1. $ ctest -S dashboard.cmake -D
    2. CTEST_CMAKE_GENERATOR="Unix Makefiles"
    3. Each . represents 1024 bytes of output
    4. . Size of output: 0K
    5. Each symbol represents 1024 bytes of output.
    6. '!' represents an error and '*' a warning.
    7. . Size of output: 1K
  8. 结果将会出现在CDash网站上:

    14.3 使用AddressSanifier向CDash报告内存缺陷 - 图1

具体实施

这个示例中,成功地向仪表板的动态分析部分报告了内存错误。我们可以通过浏览缺陷详细信息,得到进一步的了解:

14.3 使用AddressSanifier向CDash报告内存缺陷 - 图2

通过单击各个链接,可以浏览完整信息的输出。

注意,也可以在本地生成AddressSanitizer报告。这个例子中,我们需要设置ENABLE_ASAN:

  1. $ mkdir -p build
  2. $ cd build
  3. $ cmake -DENABLE_ASAN=ON ..
  4. $ cmake --build .
  5. $ cmake --build . --target test
  6. Start 1: leaky
  7. 1/2 Test #1: leaky ............................***Failed 0.07 sec
  8. Start 2: use_after_free
  9. 2/2 Test #2: use_after_free ...................***Failed 0.04 sec
  10. 0% tests passed, 2 tests failed out of 2

运行leaky测试,直接产生以下结果:

  1. $ ./build/tests/leaky
  2. =================================================================
  3. ==18536==ERROR: LeakSanitizer: detected memory leaks
  4. Direct leak of 8000 byte(s) in 1 object(s) allocated from:
  5. #0 0x7ff984da1669 in operator new[](unsigned long) /build/gcc/src/gcc/libsanitizer/asan/asan_new_delete.cc:82
  6. #1 0x564925c93fd2 in function_leaky() /home/user/cmake-recipes/chapter-14/recipe-03/cxx-example/src/buggy.cpp:7
  7. #2 0x564925c93fb2 in main /home/user/cmake-recipes/chapter-14/recipe-03/cxx-example/tests/leaky.cpp:4
  8. #3 0x7ff98403df49 in __libc_start_main (/usr/lib/libc.so.6+0x20f49)
  9. SUMMARY: AddressSanitizer: 8000 byte(s) leaked in 1 allocation(s).

相应地,我们可以直接运行use_after_free,得到详细的输出:

  1. $ ./build/tests/use_after_free
  2. =================================================================
  3. ==18571==ERROR: AddressSanitizer: heap-use-after-free on address 0x6250000004d8 at pc 0x557ffa8b0102 bp 0x7ffe8c560200 sp 0x7ffe8c5601f0
  4. READ of size 8 at 0x6250000004d8 thread T0
  5. #0 0x557ffa8b0101 in function_use_after_free() /home/user/cmake-recipes/chapter-14/recipe-03/cxx-example/src/buggy.cpp:28
  6. #1 0x557ffa8affb2 in main /home/user/cmake-recipes/chapter-14/recipe-03/cxx-example/tests/use_after_free.cpp:4
  7. #2 0x7ff1d6088f49 in __libc_start_main (/usr/lib/libc.so.6+0x20f49)
  8. #3 0x557ffa8afec9 in _start (/home/user/cmake-recipes/chapter-14/recipe-03/cxx-example/build/tests/use_after_free+0xec9)
  9. 0x6250000004d8 is located 984 bytes inside of 8000-byte region [0x625000000100,0x625000002040)
  10. freed by thread T0 here:
  11. #0 0x7ff1d6ded5a9 in operator delete[](void*) /build/gcc/src/gcc/libsanitizer/asan/asan_new_delete.cc:128
  12. #1 0x557ffa8afffa in function_use_after_free() /home/user/cmake-recipes/chapter-14/recipe-03/cxx-example/src/buggy.cpp:24
  13. #2 0x557ffa8affb2 in main /home/user/cmake-recipes/chapter-14/recipe-03/cxx-example/tests/use_after_free.cpp:4
  14. #3 0x7ff1d6088f49 in __libc_start_main (/usr/lib/libc.so.6+0x20f49)
  15. previously allocated by thread T0 here:
  16. #0 0x7ff1d6dec669 in operator new[](unsigned long) /build/gcc/src/gcc/libsanitizer/asan/asan_new_delete.cc:82
  17. #1 0x557ffa8affea in function_use_after_free() /home/user/cmake-recipes/chapter-14/recipe-03/cxx-example/src/buggy.cpp:19
  18. #2 0x557ffa8affb2 in main /home/user/cmake-recipes/chapter-14/recipe-03/cxx-example/tests/use_after_free.cpp:4
  19. #3 0x7ff1d6088f49 in __libc_start_main (/usr/lib/libc.so.6+0x20f49)
  20. 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()
  21. Shadow bytes around the buggy address:
  22. 0x0c4a7fff8040: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  23. 0x0c4a7fff8050: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  24. 0x0c4a7fff8060: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  25. 0x0c4a7fff8070: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  26. 0x0c4a7fff8080: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  27. =>0x0c4a7fff8090: fd fd fd fd fd fd fd fd fd fd fd[fd]fd fd fd fd
  28. 0x0c4a7fff80a0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  29. 0x0c4a7fff80b0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  30. 0x0c4a7fff80c0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  31. 0x0c4a7fff80d0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  32. 0x0c4a7fff80e0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  33. Shadow byte legend (one shadow byte represents 8 application bytes):
  34. Addressable: 00
  35. Partially addressable: 01 02 03 04 05 06 07
  36. Heap left redzone: fa
  37. Freed heap region: fd
  38. Stack left redzone: f1
  39. Stack mid redzone: f2
  40. Stack right redzone: f3
  41. Stack after return: f5
  42. Stack use after scope: f8
  43. Global redzone: f9
  44. Global init order: f6
  45. Poisoned by user: f7
  46. Container overflow: fc
  47. Array cookie: ac
  48. Intra object redzone: bb
  49. ASan internal: fe
  50. Left alloca redzone: ca
  51. Right alloca redzone: cb
  52. ==18571==ABORTING

如果我们在没有AddressSanitizer的情况下进行测试(默认情况下ENABLE_ASAN是关闭的),就不会报告错误:

  1. $ mkdir -p build_no_asan
  2. $ cd build_no_asan
  3. $ cmake ..
  4. $ cmake --build .
  5. $ cmake --build . --target test
  6. Start 1: leaky
  7. 1/2 Test #1: leaky ............................ Passed 0.00 sec
  8. Start 2: use_after_free
  9. 2/2 Test #2: use_after_free ................... Passed 0.00 sec
  10. 100% tests passed, 0 tests failed out of 2

实际上,泄漏只会浪费内存,而use_after_free可能会导致未定义行为。调试这些问题的一种方法是使用valgrind (http://valgrind.org )。

与前两个示例相反,我们使用了CTest脚本来配置、构建和测试代码,并将报告提交到面板。要了解此示例的工作原理,请仔细查看dashboard.cmake脚本。首先,我们定义项目名称并设置主机报告和构建名称:

  1. set(CTEST_PROJECT_NAME "example")
  2. cmake_host_system_information(RESULT _site QUERY HOSTNAME)
  3. set(CTEST_SITE ${_site})
  4. set(CTEST_BUILD_NAME "${CMAKE_SYSTEM_NAME}-${CMAKE_HOST_SYSTEM_PROCESSOR}")

我们的例子中,CTEST_BUILD_NAME的计算结果是Linux-x86_64。不同的操作系统下,可能会观察到不同的结果。

接下来,我们为源和构建目录指定路径:

  1. set(CTEST_SOURCE_DIRECTORY "${CTEST_SCRIPT_DIRECTORY}")
  2. set(CTEST_BINARY_DIRECTORY "${CTEST_SCRIPT_DIRECTORY}/build")

我们可以将生成器设置为Unix Makefile:

  1. set(CTEST_CMAKE_GENERATOR "Unix Makefiles")

但是,对于更具可移植性的测试脚本,我们更愿意通过命令行提供生成器:

  1. $ ctest -S dashboard.cmake -D CTEST_CMAKE_GENERATOR="Unix Makefiles"

dashboard.cmake中的下一个代码片段,将计算出机器上可用的CPU芯数量,并将测试步骤的并行级设置为可用CPU芯数量,以使总测试时间最小化:

  1. include(ProcessorCount)
  2. ProcessorCount(N)
  3. if(NOT N EQUAL 0)
  4. set(CTEST_BUILD_FLAGS -j${N})
  5. set(ctest_test_args ${ctest_test_args} PARALLEL_LEVEL ${N})
  6. endif()

接下来,我们开始测试步骤并配置代码,将ENABLE_ASAN设置为ON:

  1. ctest_start(Experimental)
  2. ctest_configure(
  3. OPTIONS
  4. -DENABLE_ASAN:BOOL=ON
  5. )

dashboard.cmake其他命令为映射到构建、测试、内存检查和提交步骤:

  1. ctest_build()
  2. ctest_test()
  3. set(CTEST_MEMORYCHECK_TYPE "AddressSanitizer")
  4. ctest_memcheck()
  5. ctest_submit()

更多信息

细心的读者会注意到,在链接目标之前,我们没有在系统上搜索AddressSanitizer。实际中,库查找工作已经提前做完,以避免在链接阶段出现意外。

有关AddressSanitizer文档和示例的更多信息,请参见https://github.com/google/sanitizers/wiki/AddressSanitizer 。AddressSanitizer并不仅限于C和C++。对于Fortran示例,读者可以参考 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-14/recipe-03/fortran-example

NOTE:可以在https://github.com/arsenm/sanitizers-cmake 上找到CMake程序,用来查找杀毒程序和调整编译器标志

下面的博客文章讨论了如何添加对动态分析工具的支持,对我们很有启发性:https://blog.kitware.com/ctest-cdash-add-support-for-new-dynamic-analysis-tools/