- D-6 目录规范化:docs 根目录散落文档归入 UI设计/子系统设计/架构设计/审查记录/ 环境与部署/网络 等子目录,去掉目录名空格,重命名 嵌入式C语言代码规范_V1.0.md - D-7 三档一致性门禁:新增 scripts/check_doc_links.py(链接存在性)、 scripts/gen_task_table.py(FreeRTOS 任务表数值)、scripts/check_mem_numbers.py (内存/Flash 体积),均支持 --check/--write/--diff,pre-commit 钩子同步注册 - D-5 文档索引:新建 docs/README.md,新建 docs/文档规范.md 约束目录命名与链接写法 - D-1 芯片手册瘦身:删除 1069 张无引用孤儿图片(77.06 MB), docs/芯片手册 120 MB → 41 MB,已引用图片零断链 - 同步 C 源码注释中的文档路径引用(D-6 重组后),构建 0 错误 0 警告
11 KiB
cppcheck C 静态分析 — 使用说明
本文档是
docs/代码规范/pre-commit使用说明.md的配套详解,聚焦 cppcheck 钩子的检查规则、配置参数与误报抑制方法。 pre-commit 的整体部署架构与其他钩子规则见docs/代码规范/pre-commit使用说明.md。
1. 概述
cppcheck 是开源 C/C++ 静态分析工具,不运行代码,通过语法分析和数据流推理发现潜在缺陷。
工作原理:逐个读取 C 文件 → 分析语法/数据流/控制流 → 发现问题则报错。
与 pre-commit 的关系:pre-commit 阶段的 cppcheck 钩子调用 cppcheck 命令行,任一已启用类别发现问题即阻止提交(--error-exitcode=1 对 warning/performance/portability/style 全部生效)。
日常速度
- 增量扫描(
git commit时):只检查 staged 文件,单文件约 1~2 秒 - 全库扫描(
pre-commit run --all-files):约 5 秒,用于配置变更后验证
2. 检查类别
本项目开启的类别
| 类别 | 含义 | 典型问题 |
|---|---|---|
warning |
通用警告 | 变量未使用、除零风险、空指针解引用、unreachable code |
performance |
性能问题 | 不必要的值拷贝、字符串重复查找、低效的函数调用 |
portability |
可移植性问题 | 不同平台行为不一致的代码、隐式类型转换 |
style |
代码风格 | 变量作用域过大、冗余赋值、恒假条件、自赋值等可疑写法(详见第 3 节) |
不开启的常见类别
| 类别 | 为什么不开 |
|---|---|
error |
严重错误(数组越界、内存泄漏),cppcheck 默认已包含,不需要显式加 |
information |
信息类(未解析的头文件等),无实际价值 |
3. style 类别具体检查什么(cppcheck 2.21.0)
style 是"风格/质量"级别——报告的问题不一定是 bug,但绝大多数是冗余代码、死代码、可疑写法、可以写得更安全/更清晰的代码。下面按主题列出本项目(纯 C)实际会命中的规则(--enable=style 已开启):
① 冗余与死代码(redundant & dead code)— 最值得修
| 规则 | 检查什么 | 反例 |
|---|---|---|
redundantAssignment |
变量赋值后被新值覆盖、旧值从未被读取 | ret = OK; ret = f();(首个赋值无意义) |
unreadVariable |
变量赋值后从未被读取 | s = -1; 随后循环里被覆盖、从未用到 |
redundantCondition |
短路求值使某条件恒真 | if (a && a > 5)(a 非零时前半已恒真) |
duplicateCondition |
if/else 分支条件重复 | if (x){} else if (x){} |
duplicateBreak |
连续重复 break | break; break; |
duplicateExpression / duplicateExpressionTernary |
运算符两侧表达式相同 | a = a && b、x = x ? 1 : 2 |
unreachableCode |
return 之后不会执行的代码 | return 0; foo(); |
unassignedVariable |
变量声明后从未赋值 | int x; use(x); |
unusedLabel |
从未被 goto 的标签 | label: 无对应 goto |
② 恒真/恒假条件(always-true / always-false)— 常藏逻辑 Bug
| 规则 | 检查什么 | 反例 |
|---|---|---|
knownConditionTrueFalse |
依据数据流推断条件恒真/恒假 | while (n>=365){ ... if (n<360) break; } |
knownArgument |
参数固定时函数结果可推断 | strlen("") 恒为 0 |
knownPointerToBool |
已知指针当布尔用 | p 恒非空时 if (p) |
compareValueOutOfTypeRangeError |
与类型范围外的值比较 | uint16_t x; if (x > 65535) 恒假 |
unsignedLessThanZero |
无符号 < 0 恒假 | 无符号 n 的 if (n < 0) |
unsignedPositive |
无符号 >= 0 / > -1 恒真 | if (n >= 0) |
pointerLessThanZero / pointerPositive |
指针 < 0 / > 0 | if (p < 0) |
③ const 化与作用域(const & scope)
| 规则 | 检查什么 | 备注 |
|---|---|---|
constVariable / constVariablePointer |
变量/指针可加 const 防误改 | 已全库启用并修复 |
constParameter / constParameterPointer / constParameterCallback |
参数可加 const | 回调相关两条已抑制(HAL 签名不可改) |
variableScope |
变量作用域可缩小(声明移入实际使用的最内层块) | 已全库修复 |
functionStatic / staticFunction |
函数可加 static 限制可见性 | 本库函数多被外部引用,命中少 |
④ 可疑写法(suspicious patterns)— 疑似笔误
| 规则 | 检查什么 |
|---|---|
clarifyCalculation |
位运算与逻辑运算易混(| vs ||、& vs &&) |
clarifyCondition / assignmentInCondition |
条件里出现赋值(疑似 = 误写 ==)、链式比较 |
bitwiseOnBoolean |
对布尔量做位运算 |
selfAssignment |
自赋值 x = x |
oppositeExpression |
同一条件内互相矛盾的表达式 |
moduloofone |
x % 1(恒为 0) |
mismatchingBitAnd |
位运算不匹配 |
suspiciousFloatingPointCast |
浮点强转整型(可能丢精度) |
incrementboolean |
对布尔量自增 |
shadowVariable / shadowArgument / shadowFunction |
局部变量遮蔽外层变量/参数/函数 |
arrayIndexThenCheck |
数组下标先使用、后越界检查 |
truncLongCastAssignment / truncLongCastReturn |
long 强转导致截断 |
funcArgNamesDifferent |
函数参数名与声明不一致 |
nanInArithmeticExpression |
算术表达式中出现 NaN |
ignoredReturnErrorCode |
忽略带错误码的返回值 |
unpreciseMathCall |
调用不精确的数学函数 |
returnNonBoolInBooleanFunction |
布尔函数返回非布尔值 |
multiCondition |
可疑的多条件组合 |
duplicateAssignExpression |
同一表达式赋给多个变量 |
redundantBitwiseOperationInSwitch / redundantAssignInSwitch |
switch 中冗余位运算/赋值 |
⑤ 不适用本 C 工程的规则(C++ 专属,自动跳过)
catchExceptionByValue、cstyleCast、missingOverride、noConstructor、noExplicitConstructor、operatorEqRetRefThis、unusedScopedObject、virtualCallInConstructor、useStlAlgorithm、constParameterReference 等(异常/类/STL 相关,纯 C 代码不会命中)。
说明:
- 上表规则均出自 cppcheck 2.21.0
--errorlist中severity=style的真实清单。- 部分规则(如
duplicateBranch、suspiciousSemicolon、checkConst)需要额外--inconclusive才会激活,为减少误报未开启。unusedFunction、unusedStructMember、useStlAlgorithm、noExplicitConstructor等已在--suppress中抑制,原因见第 4.3 节「抑制规则说明」。--error-exitcode=1对所有已启用级别(warning/performance/portability/style)生效:任何一级发现问题都会阻止提交。style 级别的问题若确属误报,用行内抑制(见第 5 节)。
4. 配置
4.1 配置参数
--std=c99 # 按 C99 标准检查(Keil 工程常用)
--error-exitcode=1 # 发现问题返回非零退出码(pre-commit 用来拦截提交)
--inline-suppr # 启用行内抑制注释(// cppcheck-suppress,默认关闭)
--quiet # 只报问题,不显示扫描进度
# --- 必须抑制(误报/不可修改) ---
--suppress=missingInclude # HAL/CMSIS 路径在 Keil 工程配置里,cppcheck 不知道
--suppress=invalidPrintfArgType_sint # printf 的 %d 传 uint32_t —— 实际安全
--suppress=invalidPrintfArgType_uint # printf 的 %u 传 int —— 实际安全
--suppress=constParameterPointer # HAL 回调签名固定(如 SPI_HandleTypeDef*),不能加 const
--suppress=constParameterCallback # 回调函数指针类型限制,不能改签名
--suppress=unusedFunction # 第三方库回调/弱符号函数被误报为未使用
--suppress=unusedStructMember # HAL 寄存器位域成员未显式使用
--suppress=noExplicitConstructor # C++ 兼容,不影响纯 C
--suppress=useStlAlgorithm # C++ STL 相关,不影响纯 C
# --- 头文件路径 ---
-I Inc -I App -I Drivers/BSP
-I Drivers/STM32F4xx_HAL_Driver/Inc -I Drivers/CMSIS/Include
-I Middlewares/Third_Party/FreeRTOS/Source/include
注意:以上为逻辑参数列表。在
.pre-commit-config.yaml中书写时必须遵守第 4.2 节的 YAML 引号规则。
4.2 ⚠️ YAML 参数引号陷阱(曾导致钩子完全失效)
.pre-commit-config.yaml 的 args: 使用 YAML 流序列 [a, b, c] 语法,其中逗号是元素分隔符、空格不产生新元素。曾出现两个致命错误,导致 cppcheck 钩子空转("Passed" 但从未真正检查):
| 错误写法 | 实际被解析成 | 后果 |
|---|---|---|
[--enable=warning,performance,portability,style] |
--enable=warning + 3 个假文件参数 performance/portability/style |
只启用 warning,style/performance/portability 全部失效 |
[-I Inc -I App -I Drivers/BSP] |
单个参数 "-I Inc -I App -I Drivers/BSP" |
所有 include 路径失效(指向一个不存在的目录名) |
正确写法:含逗号或空格的参数必须加引号,-I 与路径必须分成两个元素:
args: ['--enable=warning,performance,portability,style', '--error-exitcode=1',
'-I', 'Inc', '-I', 'App']
排查方法:怀疑参数被拆坏时,在 PATH 前置一个同名 shim(如 cppcheck.cmd 把 %* 写日志),跑 pre-commit run cppcheck --files <某文件> 查看实际执行的完整命令行。
4.3 抑制规则说明
| 抑制规则 | 原因 | 是否永久 |
|---|---|---|
constParameterPointer |
HAL 回调签名由驱动框架定义,用户不能改 | 永久 |
constParameterCallback |
同上,回调函数指针类型受限 | 永久 |
unusedFunction |
CubeMX 生成的弱符号、第三方库回调被误报 | 永久 |
unusedStructMember |
HAL 寄存器位域按位操作,cppcheck 不理解 | 永久 |
redundantAssignment |
HAL 清标志宏(如 __HAL_SPI_CLEAR_OVRFLAG)读寄存器覆盖旧值,cppcheck 不理解"读即目的" |
永久 |
4.4 扫描范围
通过 types: [c] + exclude 控制:
检查:App/、Src/、Inc/、Drivers/BSP/ 下的 .c 文件
不检查(第三方库,问题不是我们的):
Drivers/CMSIS/ # ARM CMSIS
Drivers/STM32F4xx_HAL_Driver/ # ST HAL
Drivers/BSP/UI/ # LVGL UI 层
Drivers/BSP/NET/lftpd/ # LwIP FTP
Drivers/BSP/dhara/ # NAND FTL
Lib/ # LVGL 库
Middlewares/ # FreeRTOS
MDK-ARM/ # Keil 工程文件
test/ # 测试代码
5. 误报抑制方法
5.1 行内抑制(// cppcheck-suppress)
确属误报、又不想全局 --suppress 掉整条规则时,在报错行的上一行加注释:
// cppcheck-suppress knownConditionTrueFalse ; 说明原因(分号后文字可选)
if (day_count < diy) break;
要点:
- 需要
--inline-suppr开关(已在配置中启用),否则注释无效 - 注释必须紧贴报错报告行的上一行(cppcheck 报告行可能不是你以为的那行,以输出为准)
- 检查 ID 后跟的
(...)括号文字会被当作 suppression attribute 解析而报错,说明文字用;后跟
5.2 全局抑制(--suppress)
某条规则在整个工程中都是噪音/不适用时,在 .pre-commit-config.yaml 的 args 中添加 --suppress=规则名(同样遵守 4.2 节引号规则),现有清单见 4.1 节。