ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Clang Static Analyzer 实战:CMake 工程接入与 GitHub Actions 自动化静态分析

Clang Static Analyzer 实战:CMake 工程接入与 GitHub Actions 自动化静态分析 先说个真实经历。我之前维护的一个C工具模块单元测试全过、CI 编译全绿、Release 都快打了结果上线当天现场崩了一次。定位了半天问题出在一个简单逻辑上某个函数在极端输入下返回空指针调用方没判空直接解引用了。这种问题编译期不报、运行时偶尔崩、单元测试还不一定覆盖得到。后来我把Clang Static Analyzer引入项目几分钟就挖出好几处同类隐患。它的核心价值就在这里在编译阶段走一遍完整的路径分析你不用真正把程序跑起来就能看到哪些地方可能在运行时出问题。这篇实战文章是我基于Clang Static Analyzer CMake GitHub Actions搭完一整套可复用分析流程的完整记录整体思路分成三块本地怎么用、CMake 工程怎么接入、CI 里怎么自动化。C/C 开发者尤其是维护 CMake 构建的团队可以照着这篇文章把静态分析从“偶尔手动跑一次”变成“每次提交自动扫描”。1. 为什么要做静态分析编译通过不等于代码安全1.1 编译器默认不深入查逻辑问题很多 C/C 开发者对静态分析的第一反应是编译器本身就有-Wall -Wextra打开不就行了这个想法能覆盖一部分问题但远远不够。GCC 和 Clang 的告警主要集中在语法、类型、未使用变量、隐式转换这类“从语法和语义上能判断出来”的问题。但对于跨语句、跨函数的路径敏感问题编译器是克制且保守的。举个例子int *get_ptr(int flag) { if (flag 0) { return NULL; } return valid_ptr; } void use() { int *p get_ptr(0); // 跟踪不到 flag 的具体值 *p 42; // 这里 p 会不会是 NULL }编译器看到get_ptr(0)只知道它返回int *至于运行时到底是不是空指针它不会去模拟执行路径。而Clang Static Analyzer做的事情恰恰是在编译时做符号执行它会跟踪flag的值、p的可能取值、走到解引用语句时p的状态从而给出“这里可能空指针解引用”的结论。所以结论很直接-Wall -Wextra解决的是“代码写得规范不规范”的问题静态分析解决的是“代码跑起来会不会炸”的问题。两者互补不冲突。1.2 为什么选 Clang Static Analyzer 而不是其他工具市面上的 C/C 静态分析工具不少我按照实际使用体验和适用场景做了个对比工具许可模式分析方式适合场景Clang Static Analyzer开源LLVM 项目路径敏感、符号执行与 Clang 编译生态天然集成Cppcheck开源规则匹配为主快速扫全局误报偏多Coverity商业深度路径分析大规模团队、企业采购PVS-Studio商业有社区版规则匹配 部分路径分析习惯 IDE 插件的团队如果你的项目已经用 Clang 或者正在用 CMakeClang Static Analyzer 的优势非常明显同源工具链不需要额外适配规则库scan-build直接包装现有构建命令就能跑GitHub Actions 的ubuntu-latest环境对 LLVM 生态支持又好。不夸张地说它是我见过的“从 0 到 1 落地成本最低”的静态分析方案。2. 环境准备CMake 与 Clang 的安装版本和 PATH 是两道坎2.1 CMake 安装的三个平台避坑先说 CMake。静态分析本身不依赖 CMake但我们分析的是一个 CMake 工程构建阶段绕不开它。这些年我见过太多因为 CMake 安装问题卡住的分析流程先在这里把坑填平。Windows 系统如果你在 PowerShell 里敲cmake --version报出下面这个错误cmake : 无法将“cmake”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。说明 CMake 没装或者装了没进 PATH。前者去 cmake.org/download 下载官方.msi安装包安装到例如C:\Program Files\CMake。后者需要手动把 CMake 的 bin 目录加入环境变量右键“此电脑” - “属性” - “高级系统设置” - “环境变量”。在“系统变量”里找到Path点“编辑”新增一行C:\Program Files\CMake\bin。重新打开 PowerShell验证cmake --version。这一步失败的根源在于 Windows 的 GUI 安装包默认不勾选“Add CMake to the system PATH”很多人装完没手动加就出现了上面的报错。装的时候也可以直接勾选该选项。Ubuntu / Debian 系统直接用sudo apt install cmake装出来的版本通常比较旧。比如 Ubuntu 20.04 默认仓库是 CMake 3.16而一些较新的FetchContent、CUDA相关特性要求 3.18 以上另外如果你要分析的项目用了CMakePresets.json又要求 3.19 之后的版本。这里我推荐从官方下载二进制包版本可控卸载也干净wget https://cmake.org/files/v3.28/cmake-3.28.1-linux-x86_64.tar.gz tar xf cmake-3.28.1-linux-x86_64.tar.gz sudo mv cmake-3.28.1-linux-x86_64 /opt/cmake sudo ln -s /opt/cmake/bin/cmake /usr/local/bin/cmake cmake --version这样装完系统里可能还残留 apt 版 CMake。为了避免混淆建议先卸载旧版sudo apt remove cmake。如果之前没有用 apt 装过那跳过这步即可。手动安装在/opt/cmake的版本卸载时直接删目录和软链接就行。macOSbrew install cmake即可没什么特别要交代的装完cmake --version验证。2.2 确认 scan-build 可用Clang Static Analyzer 的分析入口是scan-build工具。注意scan-build不一定随clang一起安装需要单独确认。在 Ubuntu 上如果执行scan-build --version提示找不到命令先安装clang-toolssudo apt-get update sudo apt-get install -y clang clang-toolsmacOS 装了 Xcode Command Line Tools 之后一般会自带。Windows 上如果通过 LLVM 官方安装包安装scan-build也包含在内但要确保它在 PATH 里。注意不同发行版对scan-build的包归属不一样。有的在clang-tools里有的在clang里。装完一定要执行scan-build --version验证这一步能省掉后面大量排查时间。3. 本地实战scan-build 包装 CMake 构建一条命令跑完整分析3.1 scan-build 是怎么“拦截”编译的scan-build的工作方式非常巧妙它不直接解析源码而是把自己的编译包装器compiler wrapper暴露给构建系统。构建系统调用gcc、g、clang这些编译器时实际上调用了scan-build的包装器包装器在真正编译之前把一份副本交给 Clang Static Analyzer 做符号执行分析编译完成后再把结果汇总成报告。这带来一个结论任何能调用编译器的构建系统理论上都能被 scan-build 分析。CMake 工程也一样只要构建命令最终是调用编译器scan-build 就能在中间拦截到。这个设计对 CMake 工程特别友好因为它不需要你改CMakeLists.txt不需要给项目增加任何依赖纯粹是“外部包装”。3.2 完整实操扫描一个 CMake 工程假设项目结构如下my-project/ ├── CMakeLists.txt └── src/ ├── main.cpp └── util.cpp先用 scan-build 包装 CMake 的配置阶段再包装构建阶段cd my-project mkdir -p build-analyze scan-build --status-bugs -o /tmp/scan-report \ cmake -S . -B build-analyze -DCMAKE_BUILD_TYPEDebug scan-build --status-bugs -o /tmp/scan-report \ cmake --build build-analyze -j$(nproc)解释一下关键参数--status-bugs如果分析器发现了 bugscan-build进程返回非零退出码。本地调试时可以不加但后面接 CI 时必须加这样才能让流水线感知到告警。-o /tmp/scan-report报告输出目录。两次包装都要加scan-build。第一次 configure 时 CMake 会做编译器探测编译器路径会在这一步被缓存到CMakeCache.txt如果只在第二步 build 时加 scan-buildCMake 可能仍调用缓存里的真实编译器导致包装失败。这里有个经验分析用的 build 目录最好单独建。不要直接复用平时编译用的build目录因为旧的CMakeCache.txt可能缓存的编译器路径和 scan-build 的 wrapper 不一致。我的习惯是固定用build-analyze作为分析专用目录。3.3 看报告scan-view 与 HTML分析完成后/tmp/scan-report下会生成一个 HTML 报告目录里面按告警级别列出了所有发现的问题。最简单的查看方式是用scan-view起一个本地服务scan-view /tmp/scan-report/2024-01-15-10-30-00-12345scan-view会起一个本地端口浏览器打开后按源码位置、缺陷类型分组展示告警还能跳转到具体的代码行。这条命令在 macOS 和 Linux 上都能直接跑排查问题时比直接翻 HTML 树高效得多。4. 把分析能力沉淀进 CMake 工程不污染代码的接入方式4.1 为什么我不建议改 CMakeLists.txt很多人接入静态分析时第一反应是往CMakeLists.txt里加option(ENABLE_STATIC_ANALYZER)、target_compile_options之类的东西把分析选项硬编码进构建脚本。我个人的建议是不要这么做。原因有两个。第一静态分析是一个“临时介入”的过程它不应该成为日常编译的一部分。把--analyze相关参数写进 CMakeLists 后每次编译都会变慢开发者容易反感最后反而把功能关掉。第二CMake 里塞分析逻辑会让构建脚本变得复杂尤其是大型项目里还要处理不同编译器的差异。相比之下用scan-build从外部包装构建命令干净利落项目本身零修改。4.2 生成 compile_commands.json给更多工具留后路虽然不改 CMakeLists 是我推荐的接入方式但有一个 CMake 选项值得打开那就是CMAKE_EXPORT_COMPILE_COMMANDS。它会在 build 目录下生成compile_commands.json把整个工程每个源文件的精确编译命令都列出来。cmake -S . -B build-analyze \ -DCMAKE_EXPORT_COMPILE_COMMANDSON \ -DCMAKE_BUILD_TYPEDebug生成之后这个文件可以被 clang-tidy、clangd、include-what-you-use 等工具复用。举个例子想用 clang-tidy 里的clang-analyzer-*检查器做补充分析clang-tidy -p build-analyze src/util.cpp \ -checks-*,clang-analyzer-*,clang-analyzer-core.NullDereference-p指定compile_commands.json所在目录它就会按这份编译数据库精确复现编译参数不会因为 include 路径缺失而产生误报。这一步的价值在于Clang Static Analyzer 查出的是整体路径问题clang-tidy 可以针对单个文件、单个源码目录做更细粒度的扫掠。两者共用同一份compile_commands.json互不冲突。4.3 封装一个 analyze.sh让团队所有人都能跑既然不改 CMakeLists那怎么让团队零门槛跑分析答案是项目根目录放一个scripts/analyze.sh把上面的命令封装起来#!/usr/bin/env bash set -euo pipefail BUILD_DIR${BUILD_DIR:-build-analyze} REPORT_DIR${REPORT_DIR:-/tmp/scan-report} rm -rf $BUILD_DIR mkdir -p $BUILD_DIR scan-build --status-bugs -o $REPORT_DIR \ cmake -S . -B $BUILD_DIR \ -DCMAKE_BUILD_TYPEDebug \ -DCMAKE_EXPORT_COMPILE_COMMANDSON scan-build --status-bugs -o $REPORT_DIR \ cmake --build $BUILD_DIR -j$(nproc) echo 报告目录: $REPORT_DIR脚本开头rm -rf $BUILD_DIR是为了保证每次都是全新目录避免 CMake 缓存导致 scan-build 静默失效。配合项目文档里“跑分析只需要执行scripts/analyze.sh”这一句话团队接入成本会低很多。5. GitHub Actions 自动化每次提交都自动跑一遍静态分析5.1 自动化要解决三个问题本地能跑通只是第一步。静态分析最大的价值在于“持续”每次代码变更都能自动扫一遍而不是等发版前手动跑一次。落到 GitHub Actions 上流水线本质上要解决三个问题拉取最新代码安装 clang、cmake、scan-build 工具链。执行与本地完全一致的分析流程。把报告传给开发者并且让流水线在发现高危问题时能给出明确信号。5.2 workflow 配置详解下面这个 workflow 是我在多个项目里跑过的、稳定可用版本name: static-analysis on: push: branches: [ main, master ] pull_request: jobs: clang-static-analyzer: runs-on: ubuntu-latest steps: - name: 检出代码 uses: actions/checkoutv4 - name: 安装工具链 run: | sudo apt-get update sudo apt-get install -y --no-install-recommends \ clang \ clang-tools \ cmake \ make - name: 运行 scan-build 静态分析 run: | rm -rf build-analyze scan-build --status-bugs -o scan-report \ cmake -S . -B build-analyze -DCMAKE_BUILD_TYPEDebug scan-build --status-bugs -o scan-report \ cmake --build build-analyze -j$(nproc) - name: 上传分析报告 if: always() uses: actions/upload-artifactv4 with: name: scan-build-report path: scan-report几个关键决策点为什么用ubuntu-latestGitHub Actions 的 Ubuntu 镜像预装了大部分构建工具虽然版本不一定最新但配合apt-get install补装clang-tools之后scan-build 一定可用。这也是成本最低的跑法。为什么显式安装cmake镜像里有自带的 CMake但版本会随着镜像更新而变化。显式装一次行为明确可复现。如果你的项目需要更高版本的 CMake这个步骤可以替换成前面第 2.1 节里的官方二进制安装逻辑。为什么if: always()要放在上传报告那一步一旦 scan-build 发现 bug--status-bugs会让这一步以非零状态退出整个 job 标记为失败。如果上传报告这一步不加if: always()job 失败后报告就不会被上传开发者拿不到任何信息。这是自动化流水线最容易踩的坑。build-analyze 目录哪里去了Actions 里的路径是工作目录下的相对路径scan-report上传 artifact 之后可以在 GitHub Actions 页面右上角“Artifacts”区域下载整个 HTML 报告。这样即使流水线失败也能在本地打开报告逐项排查。5.3 三种运行策略按团队接受度选择策略做法适用阶段只提示不阻断去掉--status-bugs报告上传job 恒为绿色刚开始接入、团队对工具还不熟悉时阻断有告警的提交保留--status-bugs有 bug 时 job 失败告警清理到可控数量后仅主干扫描on.push.branches只留mainPR 不触发想避免 PR 频繁失败的团队我个人推荐的路径先跑两周“只提示不阻断”让团队观察告警模式清理完既有问题后再加上--status-bugs。一步到位上“阻断”很容易因为告警太多而让流水线长期红着最后大家反而无视它了。6. 告警解读实战从报告里筛出真问题6.1 典型告警一空指针解引用静态分析报告里最常见、优先级最高的一类就是空指针解引用。下面是我实际项目中遇到过的模式#include string.h const char *parse_value(const char *key) { if (key nullptr || key[0] \0) { return nullptr; } // 模拟一个正常情况 return value; } void handle_config(const char *k) { const char *v parse_value(k); size_t len strlen(v); // 如果 v 为 nullptr… (void)len; }scan-build会在strlen(v)这一行标红提示 “Dereference of null pointer”。它的推理过程是当k为空字符串时parse_value返回nullptr随后strlen接收了空指针。这个推理需要跨函数跟踪一般的-Wall根本不可能发现但静态分析能沿着路径走下来。修复方式不复杂调用方判空即可关键是它能帮你在上线前发现这种“某些输入下才触发”的问题而不是等着线上崩。6.2 典型告警二资源泄漏C 语言代码里最容易出现的就是内存泄漏路径。比如#include stdlib.h char *duplicate(const char *s) { char *copy (char *)malloc(strlen(s) 1); // 这里省略拷贝逻辑 return copy; } int process(const char *input) { char *tmp duplicate(input); if (tmp NULL) { return -1; } // 某些分支直接 return没有 free(tmp) if (input[0] x) { return 0; // 泄漏tmp 未被释放 } free(tmp); return 0; }Clang Static Analyzer 会报告 “Potential leak of memory pointed to by tmp”并且指出泄漏路径从malloc到return 0。这类告警在有多个提前 return 分支的函数里特别常见手动 code review 容易漏静态分析却每次都能抓到。6.3 误报识别与屏蔽静态分析器也会有误报但不能因为偶发误报就否定整个工具。我在实践中把误报来源分成三类抽象边界导致的误报分析器对跨函数调用的建模是有限度的某些被函数指针、虚函数、外部库绕过的逻辑它可能给出不准确的结论。项目契约导致的误报代码里隐含“调用方保证传入非空”“这个函数只在某状态下调用”等约定分析器不知道。这种可以算“分析器理解力不足”而不是代码错误。环境相关误报第三方源码被扫进来出现一堆与目标代码无关的告警。针对这些情况处理方式如下排除第三方目录scan-build --status-bugs -exclude /path/to/third_party -o scan-report cmake --build build-analyze屏蔽特定检查器。如果某个 checker 在你的项目里长期误报可以直接关掉scan-build --status-bugs \ -disable-checker deadcode.DeadStores \ -disable-checker osx.cocoa.RetainCount \ -o scan-report cmake --build build-analyze用__clang_analyzer__宏跳过特殊代码段。这个方法比较冷门但很实用。Clang Static Analyzer 在分析时定义__clang_analyzer__宏普通编译时不存在因此可以在代码里做条件编译void legacy_interface(int *p) { #ifndef __clang_analyzer__ // 这段逻辑分析器无法建模且实际业务上 p 不会为空 *p 1; #endif }不过要提醒一句__clang_analyzer__是最后手段不要一上来就用。先用排除目录和 checker 配置解决不了再动代码。滥用这个宏会让分析盲区越来越大等于把刚建立的防线又拆掉。7. 排错实录从安装到 CI 的坑位复盘7.1 场景一cmake 命令找不到我在 Windows 上处理过一个同事的问题他的 PowerShell 里输入cmake --version直接报“无法将 cmake 项识别为 cmdlet”但 C 盘明明装了 CMake。根因就是 2.1 节说的 PATH 没配置。另外还有一个隐藏细节改完系统环境变量之后已经打开的 PowerShell 窗口不会自动刷新必须重开一个终端或者手动执行refreshenv需要 Chocolatey 环境。很多人在“改完 PATH 还是不行”上卡住其实就是没重开终端。7.2 场景二Ubuntu CMake 版本过低导致 configure 失败项目里用了FetchContent_Declare配合本地缓存目录的写法这套 API 对 CMake 版本有要求。Ubuntu 20.04 自带 CMake 3.16在 configure 阶段直接报错。我当时没有用 apt 折腾直接按 2.1 节拉官方二进制包解压到/opt/cmake并建立/usr/local/bin/cmake软链接一分钟解决。这里要特别说一句不要用sudo apt install cmake去覆盖旧版本apt 装新版依赖 PPA还可能与系统包冲突官方二进制包反而是最省心的。如果之前是通过 apt 安装的想干净卸载sudo apt remove cmake sudo apt autoremove然后检查which cmake如果还有残留路径多半是手动安装的旧版手动删目录和软链接即可。7.3 场景三scan-build 在 Actions 里没生效有次流水线日志显示 scan-build 跑了但最终报告里什么告警都没有。排查后发现问题出在build-analyze目录不是全新的它在 configure 阶段用了之前的CMakeCache.txt编译器路径是 GCCscan-build 的 wrapper 根本没被调用。此后的 commit 我把rm -rf build-analyze放进了命令开头问题彻底消失。这也是为什么我在 5.2 节的 workflow 里特别保留了这个清理步骤。7.4 场景四scan-build 和 Ninja 生成器配合不完整如果你在 CMake 里指定了-G Ninjascan-build也能跑但我在某些 LLVM 版本上遇到过分析报告缺失的情况尤其是并发任务较多时部分编译单元的 hook 没被扫到。定位路径比较曲折我先看scan-build标准输出发现 “analysed N files” 的数值远小于实际源码文件数再对照compile_commands.json里的文件列表确认。最终我的规避方案是在analyze.sh和 workflow 里直接指定 Unix Makefiles 生成器scan-build --status-bugs -o scan-report \ cmake -S . -B build-analyze \ -G Unix Makefiles \ -DCMAKE_BUILD_TYPEDebug分析用的构建目录和日常编译目录分开后用 Makefiles 生成器完全没有引入格式差异问题反而更稳定。7.5 场景五Actions 中 scan-build 命令找不到GitHub Actions 的ubuntu-latest镜像并不保证scan-build一定预装。我第一次跑 workflow 时在“运行 scan-build 静态分析”那一步直接报scan-build: command not found。解决方式就是 workflow 里的安装步骤sudo apt-get install -y clang clang-tools至少我测试过的 Ubuntu 22.04、24.04 镜像上装完clang-tools之后scan-build一定可用。不要漏掉这一步更不要把希望寄托在镜像的隐式预装上。整套流程跑完最直观的感受是Clang Static Analyzer 并不是一个“跑完报告看一眼”的玩具工具它真正值钱的地方在于和构建体系、CI 流水线融在一起形成持续反馈。如果你手头的项目还在靠 code review 盯着空指针和内存泄漏不妨花一个下午把本文这套流程搭起来先跑一周看看报告再决定要不要用--status-bugs把流水线卡严。就算有些告警是误报那些被它揪出来的真实问题大概率已经值回票价了。
返回列表