2.5. 跨翻译单元 (CTU) 分析

通常,静态分析在单个翻译单元 (TU) 的边界内进行。但是,通过额外的步骤和配置,我们可以使分析能够从另一个 TU 内联函数的定义。

2.5.1. 概述

CTU 分析可以以多种方式使用。导入外部 TU 定义可以与预先转储的 PCH 文件一起工作,或者在分析主 TU 期间按需生成必要的 AST 结构。静态分析的驱动也可以通过多种方式实现。最直接的方法是手动指定 Clang 前端的必要命令行选项(并手动生成特定导入方法的先决条件依赖项)。此过程可以通过其他工具(如 CodeChecker 和 scan-build-py(优先考虑前者))自动化。

2.5.2. 基于 PCH 的分析

分析需要项目中使用到的所有翻译单元的 PCH 转储。这些可以由 Clang 前端本身生成,并且必须在文件系统中以特定方式排列。索引(将符号的 USR 名称映射到包含它们的 PCH 转储)也必须由 clang-extdef-mapping 生成。如果目标是使用基于 PCH 的分析,则索引中的条目必须具有 .ast 后缀,因为缺少该扩展名表示该条目将用作源文件,并且将按需解析。此工具使用 编译数据库 来确定使用的编译标志。分析调用必须提供包含转储和映射文件的目录。

2.5.2.1. 手动 CTU 分析

让我们考虑在我们最小的示例中这些源文件

// main.cpp
int foo();

int main() {
  return 3 / foo();
}
// foo.cpp
int foo() {
  return 0;
}

以及编译数据库

[
  {
    "directory": "/path/to/your/project",
    "command": "clang++ -c foo.cpp -o foo.o",
    "file": "foo.cpp"
  },
  {
    "directory": "/path/to/your/project",
    "command": "clang++ -c main.cpp -o main.o",
    "file": "main.cpp"
  }
]

我们想分析 main.cpp 并发现除以零的错误。为了能够从 foo.cpp 内联 foo 的定义,我们首先必须生成 foo.cppAST(或 PCH)文件

$ pwd $ /path/to/your/project
$ clang++ -emit-ast -o foo.cpp.ast foo.cpp
$ # Check that the .ast file is generated:
$ ls
compile_commands.json  foo.cpp.ast  foo.cpp  main.cpp
$

下一步是创建一个 CTU 索引文件,该文件以 <USR-Length>:<USR> <File-Path> 格式保存源文件中的外部定义的 USR 名称和位置

$ clang-extdef-mapping -p . foo.cpp
9:c:@F@foo# /path/to/your/project/foo.cpp
$ clang-extdef-mapping -p . foo.cpp > externalDefMap.txt

我们必须修改 externalDefMap.txt 以包含 .ast 文件的名称,而不是源文件

$ sed -i -e "s/.cpp/.cpp.ast/g" externalDefMap.txt

我们还需要进一步修改 externalDefMap.txt 文件以包含相对路径

$ sed -i -e "s|$(pwd)/||g" externalDefMap.txt

现在所有内容都可用于 CTU 分析。我们必须为 Clang 提供特定于 CTU 的额外参数

$ pwd
/path/to/your/project
$ clang++ --analyze \
    -Xclang -analyzer-config -Xclang experimental-enable-naive-ctu-analysis=true \
    -Xclang -analyzer-config -Xclang ctu-dir=. \
    -Xclang -analyzer-output=plist-multi-file \
    main.cpp
main.cpp:5:12: warning: Division by zero
  return 3 / foo();
         ~~^~~~~~~
1 warning generated.
$ # The plist file with the result is generated.
$ ls -F
compile_commands.json  externalDefMap.txt  foo.ast  foo.cpp  foo.cpp.ast  main.cpp  main.plist
$

这种手动过程容易出错并且不可扩展,因此建议使用 CodeCheckerscan-build-py 来分析真实项目。

2.5.2.2. 使用 CodeChecker 进行自动 CTU 分析

CodeChecker 项目完全支持使用 Clang 进行自动 CTU 分析。一旦我们设置了 PATH 环境变量并激活了 python venv,就完成了所有步骤

$ CodeChecker analyze --ctu compile_commands.json -o reports
$ ls -F
compile_commands.json  foo.cpp  foo.cpp.ast  main.cpp  reports/
$ tree reports
reports
├── compile_cmd.json
├── compiler_info.json
├── foo.cpp_53f6fbf7ab7ec9931301524b551959e2.plist
├── main.cpp_23db3d8df52ff0812e6e5a03071c8337.plist
├── metadata.json
└── unique_compile_commands.json

0 directories, 6 files
$

plist 文件包含分析结果,可以使用常规分析工具查看。例如,可以使用 CodeChecker parse 在命令行中查看结果

$ CodeChecker parse reports
[HIGH] /home/egbomrt/ctu_mini_raw_project/main.cpp:5:12: Division by zero [core.DivideZero]
  return 3 / foo();
           ^

Found 1 defect(s) in main.cpp


----==== Summary ====----
-----------------------
Filename | Report count
-----------------------
main.cpp |            1
-----------------------
-----------------------
Severity | Report count
-----------------------
HIGH     |            1
-----------------------
----=================----
Total number of reports: 1
----=================----

或者我们可以使用 CodeChecker parse -e html 将结果导出到 HTML 格式

$ CodeChecker parse -e html -o html_out reports
$ firefox html_out/index.html

2.5.2.3. 使用 scan-build-py 进行自动 CTU 分析(不要这样做)

我们积极地开发使用 CodeChecker 作为驱动程序的 CTU,scan-build-py 并未积极地为 CTU 开发。scan-build-py 有各种错误和问题,预计它只适用于最基本的项目。

scan-build-py 的示例用法

$ /your/path/to/llvm-project/clang/tools/scan-build-py/bin/analyze-build --ctu
analyze-build: Run 'scan-view /tmp/scan-build-2019-07-17-17-53-33-810365-7fqgWk' to examine bug reports.
$ /your/path/to/llvm-project/clang/tools/scan-view/bin/scan-view /tmp/scan-build-2019-07-17-17-53-33-810365-7fqgWk
Starting scan-view at: http://127.0.0.1:8181
  Use Ctrl-C to exit.
[6336:6431:0717/175357.633914:ERROR:browser_process_sub_thread.cc(209)] Waited 5 ms for network service
Opening in existing browser session.
^C
$

2.5.3. 按需分析

分析在分析过程中生成外部 TU 的必要 AST 结构。这需要每个 TU 的精确编译器调用,这些调用可以通过手动生成,也可以通过驱动分析器的工具生成。编译器调用是一个 shell 命令,可用于编译 TU 的主源文件。从 TU 的绝对源文件路径到用于编译该 TU 的编译命令段列表的映射以 YAML 格式给出,称为 调用列表,并且必须作为分析器配置参数传递。索引(将函数 USR 名称映射到包含它们的源文件)也必须由 clang-extdef-mapping 生成。如果目标是使用按需分析,则索引中的条目不能具有 .ast 后缀,因为该扩展名表示该条目将用作 PCH 转储。外部定义的映射隐式地使用 编译数据库 来确定使用的编译标志。分析调用必须提供包含映射文件和 调用列表 的目录,该列表用于确定编译标志。

2.5.3.1. 手动 CTU 分析

让我们考虑在我们最小的示例中这些源文件

// main.cpp
int foo();

int main() {
  return 3 / foo();
}
// foo.cpp
int foo() {
  return 0;
}

编译数据库

[
  {
    "directory": "/path/to/your/project",
    "command": "clang++ -c foo.cpp -o foo.o",
    "file": "foo.cpp"
  },
  {
    "directory": "/path/to/your/project",
    "command": "clang++ -c main.cpp -o main.o",
    "file": "main.cpp"
  }
]

调用列表

"/path/to/your/project/foo.cpp":
  - "clang++"
  - "-c"
  - "/path/to/your/project/foo.cpp"
  - "-o"
  - "/path/to/your/project/foo.o"

"/path/to/your/project/main.cpp":
  - "clang++"
  - "-c"
  - "/path/to/your/project/main.cpp"
  - "-o"
  - "/path/to/your/project/main.o"

我们想分析 main.cpp 并发现除以零的错误。由于我们使用的是按需模式,因此我们只需要创建一个 CTU 索引文件,该文件以 <USR-Length>:<USR> <File-Path> 格式保存源文件中的外部定义的 USR 名称和位置

$ clang-extdef-mapping -p . foo.cpp
9:c:@F@foo# /path/to/your/project/foo.cpp
$ clang-extdef-mapping -p . foo.cpp > externalDefMap.txt

现在所有内容都可用于 CTU 分析。我们必须为 Clang 提供特定于 CTU 的额外参数

$ pwd
/path/to/your/project
$ clang++ --analyze \
    -Xclang -analyzer-config -Xclang experimental-enable-naive-ctu-analysis=true \
    -Xclang -analyzer-config -Xclang ctu-dir=. \
    -Xclang -analyzer-config -Xclang ctu-invocation-list=invocations.yaml \
    -Xclang -analyzer-output=plist-multi-file \
    main.cpp
main.cpp:5:12: warning: Division by zero
  return 3 / foo();
         ~~^~~~~~~
1 warning generated.
$ # The plist file with the result is generated.
$ ls -F
compile_commands.json  externalDefMap.txt  foo.cpp  main.cpp  main.plist
$

这种手动过程容易出错并且不可扩展,因此建议使用 CodeCheckerscan-build-py 来分析真实项目。

2.5.3.2. 使用 CodeChecker 进行自动 CTU 分析

CodeChecker 项目完全支持使用 Clang 进行自动 CTU 分析。一旦我们设置了 PATH 环境变量并激活了 python venv,就完成了所有步骤

$ CodeChecker analyze --ctu --ctu-ast-loading-mode on-demand compile_commands.json -o reports
$ ls -F
compile_commands.json  foo.cpp main.cpp  reports/
$ tree reports
reports
├── compile_cmd.json
├── compiler_info.json
├── foo.cpp_53f6fbf7ab7ec9931301524b551959e2.plist
├── main.cpp_23db3d8df52ff0812e6e5a03071c8337.plist
├── metadata.json
└── unique_compile_commands.json

0 directories, 6 files
$

plist 文件包含分析结果,可以使用常规分析工具查看。例如,可以使用 CodeChecker parse 在命令行中查看结果

$ CodeChecker parse reports
[HIGH] /home/egbomrt/ctu_mini_raw_project/main.cpp:5:12: Division by zero [core.DivideZero]
  return 3 / foo();
           ^

Found 1 defect(s) in main.cpp


----==== Summary ====----
-----------------------
Filename | Report count
-----------------------
main.cpp |            1
-----------------------
-----------------------
Severity | Report count
-----------------------
HIGH     |            1
-----------------------
----=================----
Total number of reports: 1
----=================----

或者我们可以使用 CodeChecker parse -e html 将结果导出到 HTML 格式

$ CodeChecker parse -e html -o html_out reports
$ firefox html_out/index.html

2.5.3.3. 使用 scan-build-py 进行自动 CTU 分析(不要这样做)

我们积极地开发使用 CodeChecker 作为驱动程序的 CTU,scan-build-py 并未积极地为 CTU 开发。scan-build-py 有各种错误和问题,预计它只适用于最基本的项目。

目前,scan-build-py 不支持按需分析。