Zircon 中的跨翻译单元静态分析

本文档介绍了以下内容:

  • 如何使用 Zircon 中的 Clang Static Analyzer (CSA) 设置交叉翻译单元分析 (CTU);
  • Kareem Khazem 在实习期间完成的工作;以及
  • Zircon 还为 CTU 完全提供支持所需的其余工作。

在 Zircon 上设置并运行 CTU

摘要:下载 Clang 的源代码,并在编译之前对其应用一些非 Mainline 补丁。围绕分析工具运行封装容器脚本。下载 CodeChecker 工具;使用该工具消化分析结果,然后启动网络服务器以通过网页界面查看结果。

支持 CTU 的补丁

有两个需要注意的补丁集:

  • Samsung 补丁集,这是一个向 Clang 添加 AST 合并支持的重要补丁。它主要包含对 lib/AST/ASTImporter.cpp 的添加。tools/xtu-build/* 下还有一组用于 CTU 分析的工具(初始工具,但目前效果不佳)。此补丁集基于 Clang 的旧版本;这一事实及其庞大的大小使得非常难以将批发到树尖 (ToT) Clang 的基础上进行批发。
  • Ericsson 补丁集,其中包括 Samsung AST 合并工作的一部分,并且添加了多个支持 CTU 分析的新工具(tools/xtu-build-new/*tools/scan-build-py/*)。xtu-build-new 工具在 Samsung 的 xtu 构建工具的基础上进行了改进,但略有不同。该补丁集比三星的补丁集更新得多,作者正在努力让其继续基于 ToT。

我们将使用 Ericsson 的补丁集修补 Clang,因为 AST 合并工作采用了干净的 rebase 技术,我们还将获得新的分析工具。但请注意,对 Zircon 的 CTU 支持尚不完善;在某些情况下,Samsung 补丁集包含提供所需功能的代码(详见下文)。

构建支持 CTU 的 CSA 的步骤

  1. 照常下载和构建 Clang 和 LLVM。
  2. 在一个单独的目录中,克隆 Ericsson 的 Clang 分支,并切换到 ctu-master 分支。
  3. 此脚本下载到 Ericsson 的分支中并运行它。该工具应将一系列补丁转储到补丁目录中。我特意只转储从爱立信更改开始时的提交内容到 1bb3636,这是我实习期间的最新修订版本。

    • 如果您希望在 Ericsson 中进行更新,可以尝试将脚本中的 1bb3636 更改为 HEAD。请务必在脚本中指定其他范围,以跳过将上游提交合并到 ctu-master 分支的提交。git log --graph 有助于确定上游提交与爱立信的提交内容分别是什么,我使用
    git log --graph  --decorate --date=relative --format=format:'%C(green)%h%C(yellow) %s%C(reset)%w(0,6,6)%C(bold green)\n%C(cyan)%G? %C(bold red)%aN%C(reset) %cr%C(reset)%w(0,0,0)\n%-D\n%C(reset)' --all
    
  4. 将生成的补丁逐一应用到上游 Clang(而不是 Ericsson 分支)中。

    for p in $(ls $PATCH_DIR/*.patch | sort -n); do git am < $p; done
    
  5. 如果尚未发布,请应用下方列出的 Kareem Khazem 的补丁

  6. 重新构建上游 Clang 和 LLVM。

进行 CTU 分析

摘要:运行我的封装容器脚本。这将正常构建 Zircon,然后重新构建,但转储序列化 AST(而不是对象文件),最后使用转储的 AST 分析每个文件,以实现 CTU。

CTU 的工作原理

首先,故事往前看:

非 CTU 静态分析会分析每个 TU 的 AST;对外部函数的任何函数调用均被视为不透明。大致上,CTU 分析会试图用该函数实现的 AST 来替换不透明的函数调用节点。

因此,CTU 分析会像往常一样开始分析 AST,但当遇到函数调用节点时,它会尝试合并到该函数的 AST。这依赖于 AST 来实现提前序列化到磁盘的函数,以便分析器可以将 AST 重新加载到内存中。它还依赖于对 AST 合并的支持,这正是 Samsung 针对 ASTImporter.cpp 的补丁(以及从中派生的 Ericsson 补丁)的用途。

为了将 AST 序列化到磁盘,我们需要模拟实际的构建流程。这种方法是在记录编译器调用的同时真正构建 Zircon;这使我们能够“播放”调用,但修改了编译器标志,用于转储 AST 文件,而不是目标文件。

简而言之,这次向前:

  • 使用 Clang 构建 zircon,并将构建流程封装在 bear 等程序中,以便记录编译器调用并生成 JSON 编译数据库。
  • 重放相同的编译步骤,但转储 AST 文件,而不是对象文件。这就是 xtu-build.py 工具的作用。
  • 照常执行静态分析,但在需要时对每个调用的函数的 AST 进行反序列化。这就是 xtu-analyze.py 工具在顶层的作用,方法是通过爱立信团队编写的精简扫描构建替换调用 scan-build-py/libscanbuild 目录中的工具。

下文提到的紫红色封装容器就记录了这些步骤。上述所有操作的结果都是一个包含所有报告的目录(Apple plist 格式),其中包含所报告 bug 的详细信息。

Ericsson 的封装容器脚本

有两套工具可用于运行交叉翻译单元分析:

  • tools/xtu-build-new 下的工具是顶级脚本。由于底层分析器可能会发生故障(即由于 CSA 崩溃),我已为 xtu-analyze.py 修补(在 Ericsson 的分支中),以便它会将分析器的输出(stdout/stderr,而不是报告)转储到文件。输出根据分析器的返回代码位于 $OUT_DIR/{passes,fails} 中,其中 $OUT_DIR 是传递给 xtu-analyze.py-o 参数的目录。这类文件中特别实用的部分是以 analyze: DEBUG: exec command in 开头的第二行,它由 libscanbuild 工具发出(下一个要点)。在修改其命令行这一漫长而繁琐的过程之后,该命令就是对 CSA 的实际调用。因此,如果您要使用 gdb 在一个有问题的文件上运行 CSA,就需要使用以下命令。
  • tools/scan-build-py 下的工具是一个“鸟”工具,用于封装对 Clang 的实际调用。他们负责修改命令行。我对他们不太熟悉,也从来没有打过他们。

Fuchsia 封装容器脚本

这个非常小的 Shell 脚本会封装 Ericsson xtu-build-new 封装容器。要对 Zircon 进行全面的分析,请务必先进行清理,并指定 Clang 构建的正确路径。然后,在 zircon 目录中执行以下操作:

ninja -t clean && ninja && ./run.sh

若要仅构建内核,请指定 TARGET 作为环境变量:

ninja -t clean && ninja clean && TARGET=./build-zircon-pc-x64/zircon.elf ./run.sh

该脚本还需要 clangify.py 位于 zircon 目录中,且已设置可执行位。分析完成后,您会看到一个 .result-xtu 目录,其中包含:

  • 一批 Apple plist 文件,它们是 bug 报告;
  • 失败目录,其中包含返回非零的分析器调用的 std{out,err};
  • 一个传递目录,其中包含返回 0 的分析器调用的 std{out,err}。

查看分析结果

目前,解析 plist 报告并通过网页界面查看它们的唯一方法是使用 CodeChecker 工具,该工具是 Ericsson 开发的,可用于代码理解和许多其他任务。CodeChecker 需要安装大量依赖项,因此最好使用 pipnpm 或任何工具进行安装,而不是使用 apt-get。简而言之,在执行分析并将 plist 转储到 .result-xtu 后,您可以调用 CodeChecker plist 来解析 plist:

CodeChecker plist -d .result-xtu -n 2016-12-12T21:47_uniq_name -j 48

每次调用 CodeChecker plist 时,-n 的参数都必须是唯一的,因为它表示单次解析运行。否则,CodeChecker 会发出错误消息。然后,运行 CodeChecker server 以在 localhost:8001 上启动 Web 服务器,该服务器会显示之前运行的所有解析的报告。

获得帮助

三星补丁集由 Aleksei Sidorin 及其团队编写。Aleksei 非常了解 ASTImporter.cpp 和其他 AST 合并方面,很有帮助。他和 Sean Callanan 非常乐意查看我的 AST Importer 补丁。Aleksei 还在 2016 年 LLVM 开发者会议上发表了相关演讲,介绍了基于总结的交互过程分析。

Ericsson 补丁集由 Gábor Horváth 及其团队编写。Gábor 就如何使用 xtu-build-new 工具进行 CTU 分析提供建议,给他们带来了很大帮助。

我 (Kareem Khazem) 也非常乐意竭尽所能为您提供帮助。

LLVM irc 通道也会有帮助。

专门针对锆石的分析

上游 Clang 非常愿意接收特定于 Zircon 的 Clang 检查工具的补丁。MutexInInterruptContext 检查工具就是一个示例(从 Farid Molazem Tabrizi 编写的 LLVM 卡券中移植),SpinLockCheckerMutexChecker 也是如此。Clang 检查的潜在审核者包括 Devin Coughlin(来自 Apple)、Artem Dergachev(三星的 Aleksei Sidorin 团队)和 Anna Zaks(也在 Apple 团队)。

这些检查工具通常是选择启用的,这意味着您需要向分析器传递一个标记(类似于 -analyzer-checker=optin.zircon.MutexInInterruptContext)来启用它们。

如果这些补丁未包含在 Clang 中,您将需要应用它们。要使用它们通过 Ericsson 封装容器脚本分析 Zircon,您应修改 Fuchsia 封装容器脚本,方法是将 -e optin.zircon.MutexInInterruptContext 选项添加到文件末尾的 xtu-analyze.py 调用中。MutexInInterruptContext 的补丁有一个测试套件,可用作分析功能的示例。

Zircon 对 CTU 的支持进展

AST 导入程序中修复的问题

上游 CSA 在绝大多数 Zircon 文件上发生崩溃。本部分介绍了 Kareem Khazem 遇到的一些问题及相应的解决方法。

不受支持的 AST 节点

由于未实现对导入某些类型的 AST 节点的支持,因此 Clang Static Analyzer 无法导入大量 Zircon 代码。下面列出了支持这些节点的补丁:

AtomicType 补丁合并到上游
CXXDependentScopeMemberExpr https://reviews.llvm.org/D26904
UnresolvedLookupExpr https://reviews.llvm.org/D27033
DependentSizedArray  
CXXUnresolvedConstructExpr  
UsingDecl https://reviews.llvm.org/D27181
UsingShadowDecl https://reviews.llvm.org/D27181
FunctionTemplateDecl https://reviews.llvm.org/D26904

通常,在实现对新节点类型的支持时,必须在 ASTImporter.cpp 中实现 VisitNode 函数,以及单元测试和功能测试;上述 Kareem 的补丁包含示例。仍然有很多不受支持的 AST 节点;使用 grep 查找 error: cannot import unsupported AST node 的分析器输出目录。

Ericsson 补丁集仅包含 Samsung 补丁集中的一部分 ASTImporter 代码。在某些情况下,可以直接从三星补丁集获取不受支持的节点的 Visit 函数。不过,由于 Samsung 补丁集不包含任何测试,因此仍需编写测试,然后才能向上游传送对该节点的支持。

海量段错误

ASTImporter.cpp 中的很多代码都是有 bug 的。有时,Aleksei 会有一些问题的专用补丁(例如这个),因此值得在 IRC 上快速 ping 他 (a-sid)。我的调试策略是在封装容器输出中查找以 analyze: DEBUG: exec command in 开头的第二个字符串(后跟分析器的实际命令行),并通过 gdb 运行该命令行。通常只需几个小时即可查明分段故障的来源。

在 CTU 前后发现 bug

VFS 中可能出现的 bug?

这是 oldparent 的二次释放,后者在 system/ulib/fs/vfs.c:vfs_rename 上声明为未初始化。两行后,调用 vfs_walk(同一文件),并使用 oldparent 作为其第二个参数。通过进入 for 循环并在第一个循环上点击 return r 语句,可以从 vfs_walk 返回,而不向 oldparent 赋值。如果 r 的值大于零,请前往 else if 语句,该语句对 oldparent(仍处于未初始化状态)调用 vn_release

线程中可能存在 bug?

这是释放后使用。路径如下:

  • kernel/kernel/thread.c:thread_detach_and_resume
    • 调用 thread_detach(t)
      • 返回 thread_join(t, NULL, 0)
        • 免费t并返回NO_ERROR
      • 返回 NO_ERROR
    • 检查错误是否为 1false1
    • 调用 thread_resume(t)(已释放)。
      • 然后,thread_resume 会访问 t 的字段。

CTU 假正例

  • CSA 无法解析通过函数指针调用的函数的实现。这意味着它不能对函数的返回值做任何假设,也不能对函数可能对输出参数产生任何影响做任何假设。
  • 有好几类函数及其实现不可供分析器访问。同样,分析器无法得知此类函数会处理其输出参数,因此会错误地报告以下代码从垃圾回收值中读取:

    struct timeval tv;
    gettimeofday(&tv, NULL);
    printf("%d\n", tv.tv_usec);   // [SPURIOUS REPORT] Access to
                                  // uninitialized variable ‘tv’
    
  • 容易受到此类不精确性影响的几种函数包括:

    • 系统调用(例如 gettimeofday
    • 编译器内置项(如 memcpy