多读书多实践,勤思考善领悟

CMake 完整使用教程 之十五 测试面板

本文于1064天之前发表,文中内容可能已经过时。

本章的主要内容有:

  • 将测试部署到CDash面板
  • CDash面板显示测试覆盖率
  • 使用AddressSanifier向CDash报告内存缺陷
  • 使用ThreadSaniiser向CDash报告数据争用

CDash是一个web服务,用于汇集CTest在测试运行期间、夜间测试期间或在持续集成中的测试结果。

本章中,我们将向CDash报告测试结果。将讨论报告测试覆盖率的策略,以及分别使用AddressSanifier和ThreadSanifier等工具,收集的内存缺陷和数据争用问题。

有两种方法向CDash报告结果:

  1. 通过构建的测试目标
  2. 使用CTest脚本

在前两个示例中使用建立测试目标的方式,在后两个示例中使用CTest脚本。

CDash环境

CDash的安装需要使用PHP和SSL的web服务器(Apache、NGINX或IIS),并访问MySQL或PostgreSQL数据库服务器。详细讨论CDash web服务的设置超出了本书的范围,读者们可以参考官方文档:https://public.kitware.com/Wiki/CDash:Installation

Kitware提供了两个面板(https://my.cdash.orghttps://open.cdash.org ),因此本章中的示例并不需要安装CDash。我们将在示例中参考已经提供的面板。

对于想要自己安装CDash的读者,我们建议使用MySQL作为后端,因为这是 https://my.cdash.orghttps://open.cdash.org 的配置方式,而且社区也对这种搭配方式进行了测试。

NOTE:也可以使用Docker来安装CDash。官方镜像的请求在CDash的跟踪器上处于打开状态,网址是https://github.com/Kitware/CDash/issues/562

14.1 将测试部署到CDash

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

本示例中,我们将扩展第4章第1节的测试示例,并将测试结果部署到https://my.cdash.org/index.php?project=cmake-cookbook ,这是在Kitware为社区提供的公共面板( https://my.cdash.org )的基础上,为本书创建的专属面板。

准备工作

我们将从重用第1节中的示例源代码,该测试将整数作为命令行参数进行求和。该示例由三个源文件组成:main.cppsum_integer.cppsum_integers.hpp。我们还将重用第4章(创建和运行测试)中的test.cpp文件,但这里将它重命名为test_short.cpp。我们将使用test_long.cpp扩展这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "sum_integers.hpp"

#include <numeric>
#include <vector>

int main() {

// creates vector {1, 2, 3, ..., 999, 1000}
std::vector integers(1000);
std::iota(integers.begin(), integers.end(), 1);

if (sum_integers(integers) == 500500) {
return 0;
} else {
return 1;
}
}

然后,将这些文件组织成以下文件树:

1
2
3
4
5
6
7
8
9
10
11
12
.
├── CMakeLists.txt
├── CTestConfig.cmake
├── src
│ ├── CMakeLists.txt
│ ├── main.cpp
│ ├── sum_integers.cpp
│ └── sum_integers.hpp
└── tests
├── CMakeLists.txt
├── test_long.cpp
└── test_short.cpp

具体实施

现在,我们将演示如何配置、构建、测试。最后,将示例项目的测试结果提交到面板的过程:

  1. 源目标在src/CMakeLists.txt中定义,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    # example library
    add_library(sum_integers "")

    target_sources(sum_integers
    PRIVATE
    sum_integers.cpp
    PUBLIC
    ${CMAKE_CURRENT_LIST_DIR}/sum_integers.hpp
    )

    target_include_directories(sum_integers
    PUBLIC
    ${CMAKE_CURRENT_LIST_DIR}
    )

    # main code
    add_executable(sum_up main.cpp)

    target_link_libraries(sum_up sum_integers)
  2. tests/CMakeLists.txt中定义了测试:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    add_executable(test_short test_short.cpp)
    target_link_libraries(test_short sum_integers)

    add_executable(test_long test_long.cpp)
    target_link_libraries(test_long sum_integers)

    add_test(
    NAME
    test_short
    COMMAND
    $<TARGET_FILE:test_short>
    )

    add_test(
    NAME
    test_long
    COMMAND
    $<TARGET_FILE:test_long>
    )
  3. CMakeLists.txt文件引用前面的两个文件,这个配置中的新元素是include(CTest),这样就可以向CDash仪表板报告结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # set minimum cmake version
    cmake_minimum_required(VERSION 3.5 FATAL_ERROR)

    # project name and language
    project(recipe-01 LANGUAGES CXX)

    # require C++11
    set(CMAKE_CXX_STANDARD 11)
    set(CMAKE_CXX_EXTENSIONS OFF)
    set(CMAKE_CXX_STANDARD_REQUIRED ON)

    # process src/CMakeLists.txt
    add_subdirectory(src)
    enable_testing()

    # allow to report to a cdash dashboard
    include(CTest)

    # process tests/CMakeLists.txt
    add_subdirectory(tests)
  4. 另外,我们创建文件CTestConfig.cmake与主CMakeLists.txt文件位于同一目录中。这个新文件包含以下几行:

    1
    2
    3
    4
    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)
  5. 我们现在已经准备好配置和构建项目:

    1
    2
    3
    4
    $ mkdir -p build
    $ cd build
    $ cmake ..
    $ cmake --build .
  6. 构建后,运行测试集,并向面板报告测试结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    $ ctest --dashboard Experimental

    Site: larry
    Build name: Linux-c++
    Create new tag: 20180408-1449 - Experimental
    Configure project
    Each . represents 1024 bytes of output
    . Size of output: 0K
    Build project
    Each symbol represents 1024 bytes of output.
    '!' represents an error and '*' a warning.
    . Size of output: 0K
    0 Compiler errors
    0 Compiler warnings
    Test project /home/user/cmake-recipes/chapter-15/recipe-01/cxx-example/build
    Start 1: test_short
    1/2 Test #1: test_short ....................... Passed 0.00 sec
    Start 2: test_long
    2/2 Test #2: test_long ........................ Passed 0.00 sec
    100% tests passed, 0 tests failed out of 2
    Total Test time (real) = 0.01 sec
    Performing coverage
    Cannot find any coverage files. Ignoring Coverage request.
    Submit files (using http)
    Using HTTP submit method
    Drop site:http://my.cdash.org/submit.php?project=cmake-cookbook
    Uploaded: /home/user/cmake-recipes/chapter-14/recipe-01/cxx-example/build/Testing/20180408-1449/Build.xml
    Uploaded: /home/user/cmake-recipes/chapter-14/recipe-01/cxx-example/build/Testing/20180408-1449/Configure.xml
    Uploaded: /home/user/cmake-recipes/chapter-14/recipe-01/cxx-example/build/Testing/20180408-1449/Test.xml
    Submission successful
  7. 最后,可以在浏览器中看到测试结果(本例中,测试结果上报到 https://my.cdash.org/index.php?project=cmake-cookbook ):

工作原理

可以从更高级的角度展示工作流,CTest运行测试并在XML文件中记录结果。然后,将这些XML文件发送到CDash服务器,在那里可以浏览和分析它们。通过单击数字2,获得关于通过或失败测试的更多的细节信息(本例中,没有失败的测试)。如下图所示,详细记录了运行测试的机器的信息,以及时间信息。同样,单个测试的测试输出也可以在线浏览。

CTest支持三种不同的提交模式:

  • 实验性构建
  • 夜间构建
  • 持续构建

我们使用了ctest --dashboard Experimental(实验性构建提交),因此,测试结果显示在实验模式之下。实验模式对于测试代码的当前状态、调试新的仪表板脚本、调试CDash服务器或项目非常有用。夜间构建模式,将把代码更新(或降级)到最接近最近夜间构建开始时的存储库,这些可以在CTestConfig.cmake中设置。其为接收更新频繁的项目的所有夜间测试提供一个定义良好的参考。例如,夜间开始时间可以设置为世界时的”午夜”:

1
set(CTEST_NIGHTLY_START_TIME "00:00:00 UTC")

持续模式对于集成工作流非常有用,它将把代码更新到最新版本。

TIPS:构建、测试和提交到实验面板只需要一个命令—cmake --build . --target Experimental

更多信息

这个示例中,我们直接从测试目标部署到CDash。我们将在本章后面的第3和第4部分中,使用专用的CTest脚本。

CDash不仅可以监视测试是否通过或失败,还可以看到测试时间。可以为测试计时进行配置:如果测试花费的时间超过分配的时间,它将被标记为失败。这对于基准测试非常有用,可以在重构代码时自动检测性能测试用例的性能情况。

有关CDash定义和配置设置的详细讨论,请参见官方CDash文档,网址为 https://public.kitware.com/Wiki/CDash:Documentation

14.2 CDash显示测试覆盖率

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

本示例中,我们将测试覆盖率报告给CDash,面板上将能够逐行浏览测试覆盖率分析,以便识别未测试或未使用的代码。

准备工作

我们将扩展前一节的源代码,在src/sum_integers.cpp中做一个小的修改,添加一个函数sum_integers_unused:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "sum_integers.hpp"

#include <vector>

int sum_integers(const std::vector integers) {
auto sum = 0;

for (auto i : integers) {
sum += i;
}

return sum;
}

int sum_integers_unused(const std::vector integers) {
auto sum = 0;

for (auto i : integers) {
sum += i;
}

return sum;
}

我们使用gcov(https://gcc.gnu.org/onlinedocs/gcc/Gcov.html )通过覆盖率分析检测这个未使用的代码。

具体实施

通过以下步骤,我们将使用覆盖率分析,并将结果上传到面板:

  1. CMakeLists.txttests/CMakeLists.txt文件与前一个示例相同。

  2. 我们将扩展src/CMakeLists.txt,并提供一个选项来添加用于代码覆盖率的编译标志。此选项默认启用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    option(ENABLE_COVERAGE "Enable coverage" ON)

    if(ENABLE_COVERAGE)
    if(CMAKE_CXX_COMPILER_ID MATCHES GNU)
    message(STATUS "Coverage analysis with gcov enabled")
    target_compile_options(sum_integers
    PUBLIC
    -fprofile-arcs -ftest-coverage -g
    )
    target_link_libraries(sum_integers
    PUBLIC
    gcov
    )
    else()
    message(WARNING "Coverage not supported for this compiler")
    endif()
    endif()
  3. 然后,配置、构建,并将结果上传CDash:

    1
    2
    3
    4
    $ mkdir -p build
    $ cd build
    $ cmake ..
    $ cmake --build . --target Experimental
  4. 最后一步,执行测试覆盖率分析:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
      Performing coverage
    Processing coverage (each . represents one file):
    ...
    Accumulating results (each . represents one file):
    ...
    Covered LOC: 14
    Not covered LOC: 7
    Total LOC: 21
    Percentage Coverage: 66.67%
    Submit files (using http)
    Using HTTP submit method
    Drop site:http://my.cdash.org/submit.php?project=cmake-cookbook
    Uploaded: /home/user/cmake-recipes/chapter-14/recipe-02/cxx-example/build/Testing/20180408-1530/Build.xml
    Uploaded: /home/user/cmake-recipes/chapter-14/recipe-02/cxx-example/build/Testing/20180408-1530/Configure.xml
    Uploaded: /home/user/cmake-recipes/chapter-14/recipe-02/cxx-example/build/Testing/20180408-1530/Coverage.xml
    Uploaded: /home/user/cmake-recipes/chapter-14/recipe-02/cxx-example/build/Testing/20180408-1530/CoverageLog-0.xml
    Uploaded: /home/user/cmake-recipes/chapter-14/recipe-02/cxx-example/build/Testing/20180408-1530/Test.xml
    Submission successful
  5. 最后,可以在浏览器中验证测试结果(本例的测试结果报告在 https://my.cdash.org/index.php?project=cmake-cookbook ):

工作原理

测试覆盖率为66.67%。为了得到更深入的了解,我们可以点击百分比,得到两个子目录的覆盖率分析:

通过浏览子目录链接,我们可以检查单个文件的测试覆盖率,甚至可以逐行浏览摘要(例如,src/sum_integs.cpp):

运行测试时,绿线部分已经被覆盖,而红线部分则没有。通过这个方法,我们不仅可以标识未使用的/未测试的代码(使用sum_integers_used函数),还可以查看每一行代码被遍历的频率。例如,代码行sum += i已经被访问了1005次(在test_short期间访问了5次,在test_long期间访问了1000次)。测试覆盖率分析是自动化测试不可或缺的功能,CDash为我们提供了一个界面,可以在浏览器中图形化地浏览分析结果。

更多信息

为了更多的了解该特性,我们推荐读者阅读下面的博客文章,它更深入的讨论了CDash的覆盖特性:https://blog.kitware.com/additional-coverage-features-in-cdash/

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
3
4
5
6
7
8
9
10
11
12
.
├── CMakeLists.txt
├── CTestConfig.cmake
├── dashboard.cmake
├── src
│ ├── buggy.cpp
│ ├── buggy.hpp
│ └── CMakeLists.txt
└── tests
├── CMakeLists.txt
├── leaky.cpp
└── use_after_free.cpp

buggy.cpp包含有两个bug:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "buggy.hpp"

#include <iostream>

int function_leaky() {
double *my_array = new double[1000];
// do some work ...
// we forget to deallocate the array
// delete[] my_array;
return 0;
}

int function_use_after_free() {
double *another_array = new double[1000];
// do some work ...
// deallocate it, good!
delete[] another_array;
// however, we accidentally use the array
// after it has been deallocated
std::cout << "not sure what we get: " << another_array[123] << std::endl;
return 0;
}

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

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

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

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

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

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

具体实施

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    add_library(buggy "")

    target_sources(buggy
    PRIVATE
    buggy.cpp
    PUBLIC
    ${CMAKE_CURRENT_LIST_DIR}/buggy.hpp
    )

    target_include_directories(buggy
    PUBLIC
    ${CMAKE_CURRENT_LIST_DIR}
    )
  2. 在文件src/CMakeLists.txt中,我们将添加一个选项用于使用ASan:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    option(ENABLE_ASAN "Enable AddressSanitizer" OFF)

    if(ENABLE_ASAN)
    if(CMAKE_CXX_COMPILER_ID MATCHES GNU)
    message(STATUS "AddressSanitizer enabled")
    target_compile_options(buggy
    PUBLIC
    -g -O1 -fsanitize=address -fno-omit-frame-pointer
    )
    target_link_libraries(buggy
    PUBLIC
    asan
    )
    else()
    message(WARNING "AddressSanitizer not supported for this compiler")
    endif()
    endif()
  3. 测试在tests/CMakeLists.txt中定义:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    foreach(_test IN ITEMS leaky use_after_free)
    add_executable(${_test} ${_test}.cpp)
    target_link_libraries(${_test} buggy)

    add_test(
    NAME
    ${_test}
    COMMAND
    $<TARGET_FILE:${_test}>
    )
    endforeach()
  4. CMakeLists.txt与之前的示例基本相同:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # set minimum cmake version
    cmake_minimum_required(VERSION 3.5 FATAL_ERROR)

    # project name and language
    project(recipe-03 LANGUAGES CXX)

    # require C++11
    set(CMAKE_CXX_STANDARD 11)
    set(CMAKE_CXX_EXTENSIONS OFF)
    set(CMAKE_CXX_STANDARD_REQUIRED ON)

    # process src/CMakeLists.txt
    add_subdirectory(src)
    enable_testing()

    # allow to report to a cdash dashboard
    include(CTest)

    # process tests/CMakeLists.txt
    add_subdirectory(tests)
  5. CTestConfig.cmake也没有修改:

    1
    2
    3
    4
    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)
  6. 这个示例中,我们使用CTest脚本向CDash提交结果;为此,我们将创建一个文件dashboard.cmake(与主CMakeLists.txt CTestConfig.cmake`位于同一个目录下):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    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_ASAN:BOOL=ON
    )

    ctest_build()
    ctest_test()

    set(CTEST_MEMORYCHECK_TYPE "AddressSanitizer")
    ctest_memcheck()

    ctest_submit()
  7. 我们将执行dashboard.cmake脚本。注意,我们使用CTEST_CMAKE_GENERATOR与生成器选项的方式:

    1
    2
    3
    4
    5
    6
    7
    8
    $ 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: 1K
  8. 结果将会出现在CDash网站上:

具体实施

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

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

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

1
2
3
4
5
6
7
8
9
10
11
$ 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测试,直接产生以下结果:

1
2
3
4
5
6
7
8
9
10
$ ./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,得到详细的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
$ ./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[](void*) /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是关闭的),就不会报告错误:

1
2
3
4
5
6
7
8
9
10
11
$ 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

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

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

1
2
3
4
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。不同的操作系统下,可能会观察到不同的结果。

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

1
2
set(CTEST_SOURCE_DIRECTORY "${CTEST_SCRIPT_DIRECTORY}")
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
2
3
4
5
6
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:

1
2
3
4
5
6
ctest_start(Experimental)

ctest_configure(
OPTIONS
-DENABLE_ASAN:BOOL=ON
)

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

1
2
3
4
5
6
7
ctest_build()
ctest_test()

set(CTEST_MEMORYCHECK_TYPE "AddressSanitizer")

ctest_memcheck()
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/

14.4 使用ThreadSaniiser向CDash报告数据争用

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

在这个示例中,我们将重用前一个示例中的方法,但是使用ThreadSanitizer或TSan,结合CTest和CDash,来检查数据竞争,并将它们报告给CDash。ThreadSanitizer的文档可以在网上找到,https://github.com/google/sanitizers/wiki/ThreadSanitizerCppManual

准备工作

这个示例中,我们将使用以下示例代码(example.cpp):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#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函数休眠1s,然后打印并递增一个整数s。我们预计此示例代码将显示数据竞争,因为所有线程读取和修改相同的地址,而不需要任何显式同步或协调。换句话说,我们期望在代码末尾打印的最终s,每次的结果都不同。代码有bug,我们将尝试在ThreadSanitizer的帮助下识别数据竞争。如果不运行ThreadSanitizer,我们可能不会看到代码有任何问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ ./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

具体实施

  1. 文件CMakeLists.txt首先定义一个受支持的最低版本、项目名称、受支持的语言。在本例中,定义了C++11标准项目:

    1
    2
    3
    4
    5
    6
    7
    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)
  2. 接下来,找到线程库,定义可执行文件,并将其链接到线程库:

    1
    2
    3
    4
    5
    6
    7
    8
    find_package(Threads REQUIRED)

    add_executable(example example.cpp)

    target_link_libraries(example
    PUBLIC
    Threads::Threads
    )
  3. 然后,提供编译选项和代码,并链接到ThreadSanitizer:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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()
  4. 最后,编译测试用例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    enable_testing()

    # allow to report to a cdash dashboard
    include(CTest)

    add_test(
    NAME
    example
    COMMAND
    $<TARGET_FILE:example>
    )
  5. CTestConfig.cmake没有变化:

    1
    2
    3
    4
    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)
  6. dashboard.cmake需要为TSan进行简单修改:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    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()
  7. 让我们以这个例子为例。通过CTEST_CMAKE_GENERATOR选项来设置生成器:

    1
    2
    3
    4
    5
    6
    7
    $ 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
  8. 在面板上,我们将看到以下内容:

  9. 我们可以看到更详细的动态分析:

工作原理

该示例CMakeLists.txt的核心部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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也需要更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# ...

ctest_start(Experimental)

ctest_configure(
OPTIONS
-DENABLE_TSAN:BOOL=ON
)

ctest_build()
ctest_test()

set(CTEST_MEMORYCHECK_TYPE "ThreadSanitizer")
ctest_memcheck()

ctest_submit()

和上一个示例一样,我们也可以在本地查看ThreadSanitizer的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ 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

更多信息

对使用OpenMP的应用TSan是很常见的,但是请注意,在某些情况下,OpenMP会在TSan下生成误检的结果。对于Clang编译器,一个解决方案是用-DLIBOMP_TSAN_SUPPORT=TRUE重新编译编译器本身及其libomp。通常,以合理的方式使用TSan可能需要重新编译整个工具堆栈,以避免误报。在使用pybind11的C++项目的情况,我们可能需要重新编译Python,并启用TSan来获得有意义的东西。或者,Python绑定可以通过使用TSan抑制而被排除在外,如 https://github.com/google/sanitizers/wiki/threadsanitizersuppression 。例如:如果一个动态库同时被一个经过TSan的二进制文件和一个Python插件调用,那么这种情况可能是不可能使用TSan。

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