CMake 秘籍(七)(5)

简介: CMake 秘籍(七)

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_ASANOFF),则以下示例不会报告任何错误:

$ 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_ASANON

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

如何操作

让我们详细地逐一介绍必要的步骤:

  1. 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)
  1. 接下来,我们定位 Threads 库,定义可执行文件,并将其与 Threads 库链接:
find_package(Threads REQUIRED)
add_executable(example example.cpp)
target_link_libraries(example
  PUBLIC
    Threads::Threads
  )
  1. 然后,我们提供选项和代码以支持 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()
  1. 最后,作为测试,我们执行编译后的示例本身:
enable_testing()
# allow to report to a cdash dashboard
include(CTest)
add_test(
  NAME
    example
  COMMAND
    $<TARGET_FILE:example>
  )
  1. 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)
  1. 相应的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()
  1. 让我们再次为这个示例设置生成器,通过传递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
  1. 在仪表板上,我们将看到以下内容:

  1. 我们可以更详细地看到动态分析如下:

它是如何工作的

本食谱的核心成分位于以下部分的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/

相关文章
|
编解码 自然语言处理
重磅!阿里巴巴开源最大参数规模大模型——高达720亿参数规模的Qwen-72B发布!还有一个的18亿参数的Qwen-1.8B
阿里巴巴开源了720亿参数规模的Qwen-72B大语言模型,是目前国内最大参数规模的开源模型。该模型在3万亿tokens数据上训练,支持多种语言和代码、数学等数据。Qwen-72B模型具有出色的评估效果,在数学逻辑和意图理解等方面超过了其他开源模型,并且支持多语言扩展。此外,阿里巴巴还开源了18亿参数规模的Qwen-1.8B模型,虽然规模较小但效果不错。Qwen-72B模型已对学术和个人完全开放,商用情况下月活低于100万可直接商用。有兴趣的用户可以通过相关链接获取模型地址和资源信息。
|
测试技术 开发工具 git
面向 C++ 的现代 CMake 教程(三)(3)
面向 C++ 的现代 CMake 教程(三)
425 0
|
Java API C++
Java JNI开发时常用数据类型与C++中数据类型转换
Java JNI开发时常用数据类型与C++中数据类型转换
500 0
|
11月前
|
人工智能 前端开发 API
一种基于通义千问prompt辅助+Qwen2.5-coder-32b+Bolt.new+v0+Cursor的无代码对话网站构建方法
本文介绍了当前大模型应用的趋势,从单纯追求参数量转向注重实际应用效果与效率,重点探讨了结合大模型的开发工具,如Bolt.new、v0、Cursor等,如何形成完整的AI工具链,助力开发者高效构建、优化和部署应用。通过实例演示了从项目创建、前端优化到后端代码改写的全过程,强调了提示词设计的重要性,并推荐了适用于不同场景的工具组合方案。
|
缓存 Linux Docker
在Docker中,镜像层级压缩如何实现?
在Docker中,镜像层级压缩如何实现?
|
C# 开发工具 git
ScreenToGif:一款开源免费且好用的录屏转Gif软件
ScreenToGif:一款开源免费且好用的录屏转Gif软件
574 1
|
缓存 网络协议 Unix
Linux 内核参数
Linux 内核参数
501 1
|
算法 开发者 索引
【C++11算法】random_shuffle和shuffle
【C++11算法】random_shuffle和shuffle
696 0
|
机器学习/深度学习 算法 PyTorch
深度学习中的图像风格迁移技术探析
图像风格迁移是近年来深度学习领域备受关注的研究方向之一。本文将从算法原理、实现步骤到应用案例,全面分析和探讨几种主流的图像风格迁移技术,为读者深入理解和应用这一技术提供详实的指南。 【7月更文挑战第2天】
618 1
|
API 网络安全 数据处理
使用Python调用远程服务器上的依赖
通过使用SSH远程执行、远程API调用和Jupyter Notebook等方法,我们可以有效地调用远程服务器上的依赖,简化本地环境配置,提高计算效率。这种方法在处理大规模数据、复杂计算任务时尤为有用。希望本文能为您提供一些有用的参考,助您在开发过程中更加高效地利用远程依赖。