C/C++错误检查工具

788 阅读11分钟

动态分析工具

内存泄漏检测工具valgrind

内部工具作用
memcheck检测内存错误:使用未初始化的内存;访问已释放的内存
cachegrind缓存分析
callgrind函数调用跟踪
helgrind用于多线程数据竞争检测

ASan

ASAN全称:Address Sanitizer,google发明的一种内存地址错误检查器。

asan会接管内存的申请和释放,每次的内存的读写都会检查,因此可以做到快速的定位踩内存的问题。

在asan之前也有其他的内存分析工具,但是asan是这些工具中比较优秀的,并不会损失大量的性能和内存(官方数据,性能下降两倍,而valgrind下降20倍)。

目前已经被集成到各大编译器中。

ASAN可以定位哪些内存使用问题

  • Heap OOB(HeapOutOfBounds 堆内存越界)
  • Stack OOB(StackOutOfBounds 栈越界)
  • Global OOB(GlobalOutOfBounds 全局变量越界)
  • UAF(UseAfterFree 内存释放后使用)
  • UAR(UseAfterReturn 栈内存回收后使用,该功能还存在少量bug,默认未开启,开启ASAN_OPTIONS=detect_stack_use_after_return=1)
  • UMR(uninitialized memory reads读取未初始化内存)
  • Leaks(内存泄露)

怎么使用ASAN工具

现在大部分编译器已经集成了支持asan的能力,编译的时候加上编译选项即可。

常见的编译选项:

  • -fsanitize=address 开起asan能力,gcc 4.8版本开启支持。
  • -fsanitize-recover=address :asan检查到错误后,不core继续运行,需要配合环境变量ASAN_OPTIONS=halt_on_error=0:report_path=xxx使用。gcc 6版本开始支持。

例子

  1. 写个bug,写一个释放后的内存还在使用的例子。
#include <stdlib.h>
int main()
{
    int *p = malloc(sizeof(int)*10);
    free(p);
    *p = 3;//该程序正常情况下并不会导致进程core,因为free后的内存被glibc的内存分配器缓存着
    return 0;
}
  1. 加上编译选项编译:gcc -fsanitize=address -g ./test.c -lasan -L /root/buildbox/gcc-10.2.0/lib64/ 其中-L指定的是libasan.so存放的位置。

  2. 指定asan的so的目录,export LD_LIBRARY_PATH=/root/buildbox/gcc-10.2.0/lib64/,执行./a.out执行程序,将可以看到asan报错。指出了内存异常使用的位置和原因。

  1. 在工程中,我们更希望程序遇到错误能不中断,而继续执行下去,我们可以使用 -fsanitize-recover=address 方法。这次我们更改下代码,多引入几个错误。
#include <stdlib.h>
int main()
{
    int *p = malloc(sizeof(int)*10);
    free(p);
    *p = 3; //错误1.释放后继续使用
    p = malloc(sizeof(int)*10);
    p[11] = 3;//错误2,越界写
    return 0;
}
  1. 编译:gcc -fsanitize=address -fsanitize-recover=address -g ./test.c -lasan -L /root/buildbox/gcc-10.2.0/lib64/

  2. 设置环境变量:export ASAN_OPTIONS=halt_on_error=0:log_path=/var/log/err.log,执行程序./a.out

  3. 查看日志路径:在/var/log目录下,形成一个err.log.212的文件,212是执行./a.out的进程号。文件记录了详细的错误信息。

ASAN的原理是什么

ASAN要记录每一块内存的可用性。把用户程序所在的内存区域叫做主内存, 而记录主内存可用性的内存区域,则叫做影子内存 (Shadow memory)。

所有主内存的分配都按照 8 字节的方式对齐。然后按照 1:8 的压缩比例对主内存的可用性进行记录,然后存入影子内存中。影子内存无法被用户直接读写, 需要编译器生成相关的代码来访问。

每一次内存的分配和释放, 都会写入影子内存。每次读/写内存区域前, 都会读取一下影子内存, 获得这块内存访问合法性 (是否被分配, 是否已被释放)。

对影子内存的写入只在分配内存的时候发生, 所以只要分配内存是多线程安全的, ASan 就是多线程安全的, 这在大部分情况下也确实成立。

计算影子内存的地址需要快速,他们采用了: 主内存地址除以 8,再加上一个偏移量的做法. 因为堆栈分别在虚拟内存地址空间的两端,这样影子内存就会落在中间。而如果用户以外访问了影子内存,那么影子内存的"影子内存"就会落到一个非法的范围 (Shadow Gap) 内,就可以知道访问出了些问题。

静态分析工具clang-tidy

1. 什么是clang-tidy

Clang-Tidy是一个由LLVM项目提供的开源工具,是一个静态分析工具,用于进行静态代码分析和代码质量改进。它利用Clang编译器的强大功能,对C++代码进行静态分析,并提供了一系列的代码改进建议和警告。不同于cppcheck使用正则表达式进行静态代码分析,Clang-Tidy是基于Clang的AST(抽象语法树)进行分析,并能检测出许多常见的编码错误和代码风格问题。包括语法错误、逻辑错误、性能问题和风格问题。

2. clang-tidy可以解决什么问题

clang-tidy可以解决各种类型的代码问题,包括:

  1. 代码风格问题:例如缩进、空格、命名规范等。
  2. 可维护性问题:例如不必要的拷贝、错误的类型转换等。
  3. 潜在的编程错误:例如空指针引用、数组越界等。
  4. 性能问题:例如慢速算法、重复计算等。

clang-tidy可以帮助开发人员在编译时发现代码中的错误,这可以帮助开发人员提高代码质量和可靠性。clang-tidy还可以帮助开发人员提高代码风格,这可以使代码更易于阅读和维护。

3. 工作原理

Clang-tidy的工作原理是将源代码传递给Clang编译器,然后通过静态分析找到代码中的问题。Clang编译器是一个基于LLVM的项目,它提供了了一个强大的C++前端,能够解析、编译和优化C++代码。而Clang-tidy则利用了Clang编译器的这些功能,对源代码进行深度分析,并找出其中可能存在的问题。

具体来说,clang-tidy使用了以下技术:

  1. 静态分析:Clang-tidy使用静态分析技术对源代码进行解析,从而构建出抽象语法树(Abstract Syntax Tree,AST)。通过AST,Clang-tidy能够深入理解代码的逻辑,找出其中可能存在的问题。
  2. 规则引擎:Clang-tidy还使用了一套规则引擎,用来检查代码是否符合特定的规则。这些规则可以是检查潜在的错误、风格问题或性能优化点等。
  3. 修复建议:对于发现的问题,Clang-tidy能够提供一些修复建议。例如,对于一个未初始化的变量,Clang-tidy可以建议将其初始化为零。

4. 如何使用clang-tidy

在Ubuntu上安装clang-tidy

主要有二种使用方式:

  1. 使用命令行

    可以直接使用命令行: clang-tidy -checks=all -p my_project my_file.cpp

    但是一般会配合githook同步使用, 在用户add或者commit的时候,触发githook,触发脚本,从而扫描用户修改的文件,达到代码静态扫描的目的。

  2. 使用cmake

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_program(CLANG_TIDY_BIN NAMES "clang-tidy")
if(CLANG_TIDY_BIN)
  set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY_BIN}" "-checks=*")
endif()
  1. 使用脚本

clang-tidy官方脚本

5. 例子

话不多说,上代码:

#include <iostream>

int main() {
    int a = 1.2;
    return 0;
}

~/test$ clang-tidy -checks=* test_lint.cpp --
7748 warnings generated.
/home/chengxumiao/test/test_lint.cpp:20:13: warning: implicit conversion from 'double' to 'int' changes value from 1.2 to 1 [clang-diagnostic-literal-conversion]
    int a = 1.2;
            ^
Suppressed 7747 warnings (7747 in non-user code).
Use -header-filter=.* to display errors from all non-system headers. Use -system-headers to display errors from system headers as well.

#include <iostream>

int main() {
    char* d = NULL;
    return 0;
}

~/test$ clang-tidy -checks=* test_lint.cpp --
7748 warnings generated.
/home/chengxumiao/test/test_lint.cpp:20:15: warning: use nullptr [modernize-use-nullptr]
    char* d = NULL;
              ^~~~~
              nullptr
Suppressed 7747 warnings (7747 in non-user code).
Use -header-filter=.* to display errors from all non-system headers. Use -system-headers to display errors from system headers as well.

再举一个例子:

struct Base {
    virtual void func() {

    }
};

struct Derive : Base {
    virtual void func() {

    }
};

这里可能我们乍一看没有任何问题,其实在C++11里派生类继承父类,重写了某些函数时最好加上override关键字,通过clang-tidy还是可以检测出来:

~/test$ clang-tidy -checks=* test_lint.cpp --
7749 warnings generated.
/home/chengxumiao/test/test_lint.cpp:14:18: warning: prefer using 'override' or (rarely) 'final' instead of 'virtual' [hicpp-use-override]
    virtual void func() {
    ~~~~~~~~~~~~~^
                        override
Suppressed 7747 warnings (7747 in non-user code).
Use -header-filter=.* to display errors from all non-system headers. Use -system-headers to display errors from system headers as well.

该工具还可以检查代码是否符合编码规范,例如Google编码规范等,看这段头文件相关代码:

#include <iostream>
#include <string>
#include <memory>

这里其实有一点点问题,头文件引用顺序不满足编码规范,这里其实clang-format都可以检测出来,但clang-tidy也可以检测出来,通过-fix还可以进行自动修复:

~/test$ clang-tidy -checks=* test_lint.cpp --
8961 warnings generated.
/home/chengxumiao/test/test_lint.cpp:2:1: warning: #includes are not sorted properly [llvm-include-order]
#include <string>
^        ~~~~~~~~
Suppressed 8960 warnings (8960 in non-user code).
Use -header-filter=.* to display errors from all non-system headers. Use -system-headers to display errors from system headers as well.

它还可以检测隐藏的内存泄漏:

int main() {
    char* ct = (char*)malloc(323);
    return 0;
}

这是使用clang-tidy的检测结果:

~/test$ clang-tidy -checks=* test_lint.cpp --
7756 warnings generated.
/home/chengxumiao/test/test_lint.cpp:20:5: warning: initializing non-owner 'char *' with a newly created 'gsl::owner<>' [cppcoreguidelines-owning-memory]
    char* ct = (char*)malloc(323);
    ^
/home/chengxumiao/test/test_lint.cpp:20:5: warning: use auto when initializing with a cast to avoid duplicating the type name [hicpp-use-auto]
    char* ct = (char*)malloc(323);
    ^~~~~
    auto
/home/chengxumiao/test/test_lint.cpp:20:11: warning: Value stored to 'ct' during its initialization is never read [clang-analyzer-deadcode.DeadStores]
    char* ct = (char*)malloc(323);
          ^
/home/chengxumiao/test/test_lint.cpp:20:11: note: Value stored to 'ct' during its initialization is never read
/home/chengxumiao/test/test_lint.cpp:20:16: warning: C-style casts are discouraged; use static_cast [google-readability-casting]
    char* ct = (char*)malloc(323);
               ^~~~~~~~~~~~~     ~
               static_cast<char*>( )
/home/chengxumiao/test/test_lint.cpp:20:16: warning: do not use C-style cast to convert between unrelated types [cppcoreguidelines-pro-type-cstyle-cast]
/home/chengxumiao/test/test_lint.cpp:20:23: warning: do not manage memory manually; consider a container or a smart pointer [cppcoreguidelines-no-malloc]
    char* ct = (char*)malloc(323);
                      ^
/home/chengxumiao/test/test_lint.cpp:21:5: warning: Potential leak of memory pointed to by 'ct' [clang-analyzer-unix.Malloc]
    return 0;
    ^
/home/chengxumiao/test/test_lint.cpp:20:23: note: Memory is allocated
    char* ct = (char*)malloc(323);
                      ^
/home/chengxumiao/test/test_lint.cpp:21:5: note: Potential leak of memory pointed to by 'ct'
    return 0;
    ^
Suppressed 7747 warnings (7747 in non-user code).
Use -header-filter=.* to display errors from all non-system headers. Use -system-headers to display errors from system headers as well.

clang-tidy还有很多高端功能,大概可以检测出250种问题,大体主要分为几大类:

  • abseil:检测abseil库的相关问题
  • android:检测Android相关问题
  • boost:检测boost库的相关问题
  • cert:检测CERT的代码规范
  • cpp-core-guidelines:检测是否违反cpp-core-guidelines
  • google:检测是否违反google编码规范
  • llvm:检测是否违反llvm编码规范
  • performance:检测性能相关的问题
  • readability:检测与可读性相关,但又不属于某些编码规范的问题
  • modernize:检测是否使用现代C++11相关的代码问题

6. vscode的clang-tidy支持

  1. 如何在VS Code中运行clang-tidy?

如果需要手动运行clang-tidy,请打开”Command Palette (Ctrl + Shift + P)”,并输入”Run Code Analysis”。你可以直接在单个文件上执行clang-tidy,也可以在所有已打开的文件上执行,也可以在整个工作区上执行。如下图所示:

如果有一些文件夹你不想在上面执行clang-tidy,则可以将它们的路径添加到”Clang Tidy: Exclude”配置项中(位于配置文件settings.json中的C_Cpp.codeAnalysis.exclude)。

你也可以设置当打开或保存一个源文件时自动执行clang-tidy。可以在Command Palette (Ctrl + Shift + P)中选择”Preferences: Open Settings (UI)”进行相关设定,如下图所示:

然后搜索”code analysis”关键字来找到所有clang-tidy相关的设置项,然后将”Clang Tidy: Enabled”设置为true。

请注意,你可以在工作区级别或者解决方案级别上进行clang-tidy全局设置。

可以通过查看蓝色的状态栏中的”火”图标来判断clang-tidy是否正在运行,如下图所示:

如果需要暂停或取消clang-tidy执行,可以点击”火”图标并选择取消或暂停执行:

  1. 如何配置clang-tidy检查规则?

如果你的项目目录中有 .clang-tidy 配置文件,C++ 扩展将遵守该文件中定义的检查和选项。 如果你的工作区中有多个 .clang-tidy 配置文件,clang-tidy 将通过在它的上一级目录中搜索路径来使用最接近源文件的配置文件。 或者,你也可以使用 Clang Tidy: Config 设置指定 clang-tidy 配置。 Clang Tidy:Config 接受检查和检查选项作为 YAML/JSON 格式的字符串。

如果源文件在其任何上一级目录中都没有 .clang-tidy 配置文件,并且 Clang Tidy: Config 属性留空,则回退配置(在C_Cpp.codeAnalysis.clangTidy.fallbackConfig 中定义)将是 用于该文件。

你可以使用 Clang Tidy > Checks: Enabled 和 Clang Tidy > Checks: Disabled 设置启用和禁用更多检查。 除了 .clang-tidy 文件中的检查外,还会运行这些设置中定义的检查。

选择添加项目会显示所有 clang-tidy 检查的列表。

你可以向 Clang Tidy > Checks: Enabled 和 Clang Tidy > Checks: Disabled 设置添加任意数量的检查。

  1. 如何将命令行参数传递给 clang-tidy?

如果你通过命令行选项传递给 clang-tidy,则可以在 Clang Tidy: Args 属性中指定这些选项。 Clang Tidy: Args 设置优先于编辑器中等效的 Clang Tidy 设置(例如 Clang Tidy > Checks: Enabled 和 Clang Tidy > Checks: Disabled)。

  1. 在编辑器中查看检查结果

clang-tidy 检查的结果(警告和错误)显示在问题面板中,并在相关代码部分下方显示为波浪线。

单击“问题”面板中的问题会将您带到源文件中的问题。 要清除代码分析波浪线,请单击快速操作灯泡,然后选择清除代码分析波浪线。

友情提示:如果你想取消对特定代码段的 clang-tidy 分析,可以在文件中添加 NOLINT、NOLINTNEXTLINE 和 NOLINTBEGIN至NOLINTEND之间的注释。