LLVM 之 Clang 静态分析器篇(1):如何使用 Clang 静态分析器

Author: stormQ

Created: Sunday, 04. April 2021 03:13PM

Last Modified: Monday, 05. April 2021 07:25PM



摘要

本文介绍了如何在 Linux 系统环境中使用命令行界面在简单项目中使用 Clang 静态分析器,以及使用Scan-BuildCodeChecker这两种工具来分析大型项目的方法。


Clang 静态分析器简介

静态分析是在语法分析之后,但在编译之前完成的。

Clang 静态分析器可以自动分析庞大的代码库,有助于在编译代码之前检测出 C、C++ 和 Objective-C 中的各种常见编程错误。

Clang 静态分析器通过一组检查器来构建详细的错误报告,而每个检查器负责查找特定的错误类型。

相比运行在编译器前端的简单错误检测工具而言,Clang 静态分析器更准确,但其准确性和计算时间之间存在权衡关系。


检查器的分类

检查器的名称遵循标准格式<package>.<subpackage>.<checker>,从而便于只运行一组特定的检查器。也就是说,Clang 静态分析器既允许用户指定检查器子集,也允许启用所有检查器。

Clang 12 中,默认检查器的分类如下:

检查器的 package 名称 用途
core 通用检查器
cplusplus 用于 C++ 程序的检查器
deadcode 用于检查无实际意义的代码
fuchsia 用于 Fuchsia(Google 开发的操作系统)API 误用的检查器
nullability 用于 Objective-C 程序的检查器
optin 用于可移植性、性能以及编码风格方面的检查
osx 专门用于 Mac OS X 程序的检查器
security 代码安全漏洞检查器
unix 专门用于 UNIX 程序的检查器
valist
webkit 用于 WebKit(web 浏览器引擎)程序的检查器

查看默认的检查器列表,执行命令如下:

$ clang -cc1 -analyzer-checker-help

输出如下:

OVERVIEW: Clang Static Analyzer Checkers List

USAGE: -analyzer-checker <CHECKER or PACKAGE,...>

CHECKERS:
  core.CallAndMessage           Check for logical errors for function calls and Objective-C message expressions (e.g., uninitialized arguments, null function pointers)
  core.DivideZero               Check for division by zero
// 省略...

另外,可以通过命令clang -cc1 -analyzer-checker-help-alpha查看alpha检查器(开发中的检查器)。可以通过命令clang -cc1 -analyzer-checker-help-developer查看Debug检查器(用于调试的检查器)。


如何在简单项目中使用 Clang 静态分析器

本节介绍了在 Linux 系统环境中适用于简单项目的 Clang 静态分析器的使用方法。

step 1: 编写测试程序

1) 测试程序 1

测试程序 hello_w1.cpp 的内容如下:

#include <stdio.h>

int main()
{
  int i;
  printf("Hello, World! %d, %d\n", i, i / 0);
  return 0;
}

上面的测试程序中,有如下两个编程错误:

2) 测试程序 2

测试程序 hello_w2.cpp 的内容如下:

#include <stdio.h>
#include <stdlib.h>

int main()
{
  int *i = (int*)malloc(sizeof(int));
  *i = 1;
  printf("Hello, World! %d\n", *i);
  return 0;
}

上面的测试程序中,有如下一个编程错误:未释放由malloc分配的内存。

step 2: 运行 Clang 静态分析器

1) 方式 1——通过编译器驱动程序 clang

a) 启用指定包的所有检查器

$ clang --analyze -Xanalyzer -analyzer-checker=<package> <source-files>

注:

b) 启用指定的检查器

$ clang --analyze -Xanalyzer -analyzer-checker=<package.subpackage.checker> <source-files>

注:如果要同时指定多个检查器,则形如:clang --analyze -Xanalyzer -analyzer-checker=core.DivideZero -Xanalyzer -analyzer-checker=cplusplus.NewDelete hello.cpp

需要注意的是, 通过编译器驱动程序运行 Clang 静态分析器的方式,会根据系统环境默认提供一些检查器。

比如,执行如下命令时未显式指定任何检查器,但仍可以检测出“除 0”错误。如下所示:

$ clang --analyze hello_w1.cpp
hello_w1.cpp:6:41: warning: Division by zero [core.DivideZero]
  printf("Hello, World! %d, %d\n", i, i / 0);
                                      ~~^~~
1 warning generated.

出现上述结果的原因在于,编译器驱动程序默认提供的检查器中包括core的所有检查器。我们可以通过-###打印其详细参数,如下所示:

$ clang --analyze hello_w1.cpp -###
clang version 12.0.0 (省略 ...)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /usr/local/bin
 (in-process)
 "/usr/local/bin/clang-12" "-cc1" "-triple" "x86_64-unknown-linux-gnu" "-analyze" "-disable-free" "-main-file-name" "hello_w1.cpp" "-analyzer-store=region" "-analyzer-opt-analyze-nested-blocks" "-analyzer-checker=core" "-analyzer-checker=apiModeling" "-analyzer-checker=unix" "-analyzer-checker=deadcode" "-analyzer-checker=cplusplus" "-analyzer-checker=security.insecureAPI.UncheckedReturn" "-analyzer-checker=security.insecureAPI.getpw" "-analyzer-checker=security.insecureAPI.gets" "-analyzer-checker=security.insecureAPI.mktemp" "-analyzer-checker=security.insecureAPI.mkstemp" "-analyzer-checker=security.insecureAPI.vfork" "-analyzer-checker=nullability.NullPassedToNonnull" "-analyzer-checker=nullability.NullReturnedFromNonnull
 省略 ...

从上面的输出可以看出,编译器驱动程序默认提供的检查器有coreunixdeadcodecplusplus等。

需要注意的是, 上述命令的静态分析结果中只输出了除 0的警告信息,而未打印使用未初始化的变量 i的警告。也就是说,Clang 静态分析器可能不会一次性输出所有错误。这提示我们在修复警告后最好再次运行 Clang 静态分析器。

c) 禁用指定的检查器

$ clang --analyze -Xanalyzer -analyzer-disable-checker=<arg> <source-files>

使用示例:

未禁用unix.Malloc检查器时,输出如下(可以检测到内存泄露):

$ clang --analyze hello_w2.cpp
hello_w2.cpp:8:3: warning: Potential leak of memory pointed to by 'i' [unix.Malloc]
  printf("Hello, World! %d\n", *i);
  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 warning generated.

禁用unix.Malloc检查器后,输出如下(无法检测到内存泄露):

$ clang --analyze -Xanalyzer -analyzer-disable-checker=unix hello_w2.cpp

2) 方式 2——通过编译器 clang -cc1

如果要真正实现只运行我们指定的检查器,则应该通过编译器clang -cc1的方式运行 Clang 静态分析器。

a) 启用指定包的所有检查器

$ clang -cc1 -w -fcolor-diagnostics -analyze -analyzer-checker=<package> <source-files> -I<directory>

注:

使用示例:

$ clang -cc1 -w -fcolor-diagnostics -analyze -analyzer-checker=core hello_w1.cpp -I/usr/include/ -I/usr/local/lib/clang/12.0.0/include
hello_w1.cpp:6:41warning: Division by zero [core.DivideZero]
  printf("Hello, World! %d, %d\n", i, i / 0);
                                      ~~^~~
1 warning generated.

需要注意的是, 如果不使用-w选项,则可能出现重复的警告。如下所示:

$ clang -cc1 -fcolor-diagnostics -analyze -analyzer-checker=core hello_w1.cpp -I/usr/include/ -I/usr/local/lib/clang/12.0.0/include
hello_w1.cpp:6:41warning: division by zero is undefined [-Wdivision-by-zero]
  printf("Hello, World! %d, %d\n", i, i / 0);
                                        ^ ~
hello_w1.cpp:6:41: warning: Division by zero [core.DivideZero]
  printf("Hello, World! %d, %d\n", i, i / 0);
                                      ~~^~~
2 warnings generated.

b) 启用指定的检查器

$ clang -cc1 -w -fcolor-diagnostics -analyze -analyzer-checker=<package.subpackage.checker> <source-files>

step 3: 导出静态分析结果

我们可以通过如下命令查看 Clang 静态分析器提供的所有命令。

$ clang cc1 -help | grep analyzer

其中,与--analyzer-output标志相关的内容如下:

  --analyzer-output <value>
                          Static analyzer report output format (html|plist|plist-multi-file|plist-html|sarif|text).

我们可以通过--analyzer-output标志指定静态分析结果的文件格式。命令如下:

$ clang -cc1 -w -fcolor-diagnostics -analyze -analyzer-checker=<package.subpackage.checker> <source-files> -analyzer-output <format> -o <output-file>

注: -analyzer-output <format>选项,表示要保存的文件格式为<format>,形如-analyzer-output plist-o <output-file>选项,用于指定输出文件。

需要注意的是, 在 Linux 系统环境中,Clang 12 目前仅支持plist格式(即 XML 格式)。

使用示例:

$ clang -cc1 -fcolor-diagnostics -analyze -analyzer-checker=core hello_w1.cpp -I/usr/include/ -I/usr/local/lib/clang/12.0.0/include -analyzer-output plist -o a

如何利用 Scan-Build 在大型项目中使用 Clang 静态分析器

本节介绍了在 Linux 系统环境中利用 Clang 静态分析器自带的工具——scan-build检查大型项目的方法。

scan-build的工作原理是替换用于定义 C/C++ 编译器命令的CCCXX环境变量,从而干涉正常的项目构建过程。

scan-build是在源码库编译的同时开展分析过程的。这隐含着,scan-build不会分析不被编译的文件。一旦编译完成,scan-build将生成可以在浏览器中查看的HTML格式的错误报告。

需要注意的是, scan-build目前不支持跨编译单元分析。

step 1: 生成错误报告

1) 运行 scan-build

要使用scan-build,只要在构建命令的最前面添加scan-build即可,如下所示:

$ scan-build [scan-build options] <command> [command options]

常用的scan-build options如下表所示。

scan-build options 用途
-o <output location> 指定分析结果的存储位置
-v 输出分析过程,可以连续使用两个或三个-v以提高输出信息的详细度。
-V--view 在生成错误报告完成后,立即在浏览器中查看(即自动调用scan-view命令)

要查看所有的scan-build options,可以通过scan-build --help命令。

使用示例:

$ scan-build -v -v -v -o . gcc hello_w1.cpp

输出如下:

scan-build: Using '/usr/local/bin/clang-12' for static analysis
scan-build: Emitting reports for this run to '/home/xxq/Desktop/tt/2021-04-05-153902-351375-1'.
hello_w1.cpp: In function ‘int main()’:
hello_w1.cpp:6:41: warning: division by zero [-Wdiv-by-zero]
    6 |   printf("Hello, World! %d, %d\n", i, i / 0);
      |                                       ~~^~~
gcc hello_w1.cpp

[LOCATION]: /home/xxq/Desktop/tt
#SHELL (cd '/home/xxq/Desktop/tt' && '/usr/local/bin/clang-12' '-cc1' '-triple' 'x86_64-unknown-linux-gnu' '-analyze' '-disable-free' '-main-file-name' 'hello_w1.cpp' '-analyzer-store=region' '-analyzer-opt-analyze-nested-blocks' '-analyzer-checker=core' '-analyzer-checker=apiModeling' '-analyzer-checker=unix' '-analyzer-checker=deadcode' '-analyzer-checker=cplusplus' '-analyzer-checker=security.insecureAPI.UncheckedReturn' '-analyzer-checker=security.insecureAPI.getpw' '-analyzer-checker=security.insecureAPI.gets' '-analyzer-checker=security.insecureAPI.mktemp' '-analyzer-checker=security.insecureAPI.mkstemp' '-analyzer-checker=security.insecureAPI.vfork' '-analyzer-checker=nullability.NullPassedToNonnull' '-analyzer-checker=nullability.NullReturnedFromNonnull' '-analyzer-output' 'plist' '-w' '-setup-static-analyzer' '-mrelocation-model' 'static' '-mframe-pointer=all' '-fmath-errno' '-fno-rounding-math' '-mconstructor-aliases' '-munwind-tables' '-target-cpu' 'x86-64' '-tune-cpu' 'generic' '-fno-split-dwarf-inlining' '-debugger-tuning=gdb' '-resource-dir' '/usr/local/lib/clang/12.0.0' '-internal-isystem' '/usr/lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9' '-internal-isystem' '/usr/lib/gcc/x86_64-linux-gnu/9/../../../../include/x86_64-linux-gnu/c++/9' '-internal-isystem' '/usr/lib/gcc/x86_64-linux-gnu/9/../../../../include/x86_64-linux-gnu/c++/9' '-internal-isystem' '/usr/lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9/backward' '-internal-isystem' '/usr/local/include' '-internal-isystem' '/usr/local/lib/clang/12.0.0/include' '-internal-externc-isystem' '/usr/include/x86_64-linux-gnu' '-internal-externc-isystem' '/include' '-internal-externc-isystem' '/usr/include' '-fdeprecated-macro' '-fdebug-compilation-dir' '/home/xxq/Desktop/tt' '-ferror-limit' '19' '-fgnuc-version=4.2.1' '-fcxx-exceptions' '-fexceptions' '-analyzer-display-progress' '-analyzer-output=html' '-faddrsig' '-o' '/home/xxq/Desktop/tt/2021-04-05-161437-357805-1' '-x' 'c++' 'hello_w1.cpp')
ANALYZE (Syntax): hello_w1.cpp main()
ANALYZE (Path,  Inline_Regular): hello_w1.cpp main()
hello_w1.cpp:6:41: warning: Division by zero [core.DivideZero]
  printf("Hello, World! %d, %d\n", i, i / 0);
                                      ~~^~~
1 warning generated.
scan-build: Analysis run complete.
scan-build: 1 bug found.
scan-build: Run 'scan-view /home/xxq/Desktop/tt/2021-04-05-153902-351375-1' to examine bug reports.

从上面的输出可以看出,错误报告的存储路径为/home/xxq/Desktop/tt/2021-04-05-153902-351375-1

step 2: 查看错误报告

1) 方式 1

我们可以在浏览器中打开错误报告目录中的index.html文件来查看错误报告。如下所示(部分):


如何利用 CodeChecker 在大型项目中使用 Clang 静态分析器

待补充


References


下一篇:LLVM 之 Clang 静态分析器篇(2):如何扩展 Clang 静态分析器

上一篇:上一级目录

首页