↓ 跳过正文
  1. 嵌入式学习笔记/
  2. EmbeddedStudy/

用静态分析回溯可能的空指针路径

目录
EmbeddedStudy - 这篇文章属于一个选集。
§ 10: 本文

本文由 AI 辅助生成并经自动审查;尚未完成真实硬件验证。涉及具体芯片、时序和电气行为时,请以文末第一方资料和实际测试为准。

第 10 课:用静态分析回溯可能的空指针路径
#

编译成功只能说明工具接受了代码,不代表每条可能路径都安全;只运行一组输入,也可能绕开风险。本课先人工预测一条“返回空指针后继续解引用”的路径,再让 GCC 静态分析器在不运行被分析程序时寻找它。分析器报告是待核对的风险证据,不是程序正确性证明。

1. 本节目标
#

学习者能在 Windows 主机上对一个无动态内存的最小 C11 源文件运行 GCC 静态分析器,说明分析器何时读取什么代码,沿条件、返回值和解引用点回溯一条可能路径,并通过受控修复复查报告。预计用时 30 分钟。

本课只引入计划规定的两个新核心概念:

  1. 静态分析在不执行程序时沿可能路径发现风险。
  2. 静态分析诊断需要回溯到代码路径并以证据处理。

2. 它解决什么问题
#

普通编译会完成语法、类型等检查,但一次成功编译不能证明所有可能输入都安全。主机运行也只覆盖实际走过的路径。静态分析器(static analyzer,静态分析工具)读取源代码及工具可见的调用关系,推断可能到达的状态,在程序运行前提示风险。

GCC 15.2.0 手册说明,-fanalyzer 会分析程序流并寻找跨函数的“有意义路径”;它并非完备或可靠的证明系统,可能产生误报(false positive,报告了实际约束下不会发生的风险)和漏报,因此定位是找缺陷而非证明程序正确。[1]

3. 必要前置知识
#

本课沿用已完成的 C11 最小程序、固定宽度整数、指针、const、存储期、链接属性、编译器和主机构建调用。不假定你知道控制流图、符号执行、MISRA C、覆盖率、动态检测、交叉编译或 STM32 硬件。

空指针(null pointer)是不指向有效对象的指针值。空指针解引用(null-pointer dereference)是通过它读取或写入对象;本课只把它作为一条易观察的受控风险路径,不扩展指针课程。

4. 核心原理
#

GCC 在收到 -fanalyzer 后,会在编译期间分析当前可见的 C 代码,尝试串起条件分支和函数调用。GCC 15.2.0 明确把它称为对代码的符号执行,并说明分析成本高于普通告警;-Wanalyzer-null-dereference 针对“已知为空的值在某条路径上被解引用”,而 -Wanalyzer-possible-null-dereference 针对“可能为空”的路径。[1]

可能执行路径(possible execution path)是条件和调用关系允许经历的一条路线,不等于这条路线已经在主机或硬件上发生。分析器也会做取舍,所以有报告时要回溯证据;没有报告时仍不能推出“没有缺陷”。

风险处置(triage,风险分类与处理)不是清空告警数量,而是回答:哪个条件成立、哪个函数返回了什么、哪个使用点产生风险、修复后路径是否被代码边界阻断。怀疑误报时也要记录实际约束的证据,不能凭直觉忽略。

5. 关键术语与直观模型
#

术语 本课直观含义
GCC 静态分析器 GCC 中由 -fanalyzer 启用的具体工具实例
分析器诊断 工具针对可能代码路径给出的风险提示
误报 报告的路径在已证实约束下不可发生
风险处置 定位、解释、修复或带证据保留报告
Host Fake(主机替身) 用主机工具验证分析流程,不代表 MCU 硬件

必须区分四类结果:

活动 能说明什么 不能说明什么
普通编译诊断 编译器对当前代码和选项给出的信息 所有路径安全
静态分析器诊断 工具推断出一条值得检查的路径 路径已在运行时发生
主机运行/测试 某组输入在主机上的实际结果 未执行路径或 STM32 行为
硬件验证 有记录的目标板构建、运行或测量结果 本课没有此类证据

6. 从输入到结果的完整流程
#

app.c
  -> 学习者传入文件、C11 选项和 -fanalyzer
  -> GCC 在编译期间读取源码并分析可能路径
  -> 终端返回诊断与退出状态
  -> 学习者回溯“条件 -> 返回值 -> 解引用点”
  -> 受控修复 -> 使用同一输入重新分析 -> 比较证据
  • 解决的问题:在运行前暴露可能风险。
  • 定义在哪里:源码和受控路径定义在 app.c。
  • 谁持有:文件系统持有源码、可选目标文件与诊断日志。
  • 谁注入:学习者或构建后端把文件名与选项交给 GCC;这是工具输入,不是运行时依赖注入。
  • 调用如何流动:app.c -> GCC 分析器 -> 路径诊断 -> 人工回溯 -> 修复 -> 再分析。

app_read 的声明与实现都在单文件示例中;静态数组是代码定义的数据对象,程序加载时具有静态存储期。本例没有架构对象的实例化、BSP 组装或依赖注入,也没有运行时框架调用。

7. 嵌入式系统中的对应位置
#

同一机制可用于嵌入式 C,但选项属于具体工具,不属于 C11。迁移到 STM32 工程时,必须按实际 GCC/Arm 工具链版本重新核对分析功能、纳入分析的源文件、宏和编译选项;Arm Compiler 的命令行应查其自身 Reference Guide。[3] 本课不据此宣称 Arm 工具存在与 GCC -fanalyzer 等价的功能。

没有具体开发板,因此不讨论引脚、时钟树、外设实例、寄存器、HAL/LL 或 F4/F7/H5/H7 差异。

8. 主机实验与硬件迁移边界
#

Host Fake 能验证本机 GCC 是否接受选项、是否产生诊断,以及源码受控变化是否改变该诊断。它不能验证 STM32 交叉工具链、CPU 故障行为、下载或板上运行。

本次运行没有编译、执行或上板记录。下面代码是示例,待主机执行;迁移后仍待上板验证,hardware_verified=false。

9. 最小代码示例
#

分析前先预测:main 传入 1U;find_value 只有在键为 0U 时返回有效地址,因此返回 NULL;app_read 没有检查便执行 *value。

app.c 属于应用示例层。values 具有静态存储期,value 是调用期间存在的局部指针;依赖只指向 C 标准头,不使用动态内存:

#include <stddef.h>
#include <stdint.h>

static const uint32_t values[1] = {42U};

static const uint32_t *find_value(uint32_t key)
{
    if (key == 0U) {
        return &values[0];
    }
    return NULL;
}

int32_t app_read(uint32_t key)
{
    const uint32_t *const value = find_value(key);
    return (int32_t)(*value);
}

int main(void)
{
    return (app_read(1U) == 0) ? 0 : 1;
}

受控修复只改变使用边界,在解引用前判断:

int32_t app_read(uint32_t key)
{
    const uint32_t *const value = find_value(key);
    if (value == NULL) {
        return -1;
    }
    return (int32_t)(*value);
}

修复阻断了本课预测的空指针解引用路径,但不证明整个程序正确。

10. 常见错误
#

  • 把 -Wall -Wextra 与 -fanalyzer 当成同一种检查;GCC 手册把额外静态分析作为单独选项说明。[1][2]
  • 只数告警,不看条件、返回值与解引用点。
  • 把特定告警文字、编号、退出状态或目标文件生成情况当成跨版本保证。
  • 看到诊断消失就宣布修复正确,没有比较源码、命令和输入散列。
  • 用“我认为不会传入 1”判定误报,却没有调用方约束或测试证据。
  • 把主机分析结果误写成构建、运行或上板已经完成。

11. 调试观察点
#

  1. 版本:保存 gcc --version;-fanalyzer 还要求该 GCC 构建启用了分析器支持。[1]
  2. 输入:记录 app.c 的 SHA-256 散列,避免比较到不同源码。
  3. 命令:完整记录语言标准、告警、分析器和 -c 选项。
  4. 路径:在实际输出中找条件分支、find_value 返回点和 *value 使用点;措辞以本机结果为准。
  5. 状态:紧接命令记录 $LASTEXITCODE。告警是否导致非零状态取决于诊断策略;例如 GCC 的 -Werror 才会把告警转成错误。[2]
  6. 产物:检查日志和 app.o 是否存在,但不要预设一定生成。

12. 实战实验
#

在新的实验目录创建上述危险版本 app.c,先写下预测路径,再执行:

gcc --version
Get-FileHash .\app.c -Algorithm SHA256
gcc -std=c11 -Wall -Wextra -Wpedantic -fanalyzer `
    -c .\app.c -o .\app.o 2>&1 |
    Tee-Object .\analyzer-before.log
$LASTEXITCODE

保存版本、完整命令、散列、退出状态、完整诊断和产物列表。不要预写期望的告警编号或原文。随后只替换 app_read 为修复版本,重新记录散列,用同一命令生成 analyzer-after.log,比较危险路径是否还可到达。

失败排查:

  • 找不到 gcc:保存错误,检查安装与 PATH,不声称已分析。
  • 不识别 -fanalyzer:记录版本与完整输出,查对应版本官方手册;不要换成人工阅读后宣称工具验证完成。
  • 没有预期报告:先核对输入文件、命令和散列,再确认调用确实从 1U 到达解引用;把结果记录为“未触发”,不能写成“程序正确”。
  • 修复后仍报告:从第一条路径事件重新回溯,确认日志来自新散列,并检查是否还有另一条解引用路径。

13. 自检题
#

  1. 静态分析与实际运行最关键的区别是什么?
  2. 本例的条件、空返回值和解引用点分别在哪里?
  3. 谁持有源码和日志,谁把选项交给 GCC?为什么这不是依赖注入?
  4. 为什么“无诊断”不能证明程序正确?
  5. 判定误报时至少要保存哪些路径证据?

14. 面试题
#

问:静态分析报告了一条空指针路径,你会怎样处理?

答题要点:先固定工具版本、命令和源码;按报告回溯条件、返回值与使用点;判断路径约束是否真实;做最小修复或记录可证明的约束;在相同输入下复查,并用运行测试补充而不是替代静态证据。不能把报告或报告消失直接等同于硬件事实。

15. 延伸思考
#

  • 若 key 来自外部模块,什么证据才能证明某个值永远不会传入?
  • 为什么增加空检查能阻断本路径,却仍不能证明其他路径没有问题?
  • 迁移到 STM32 工程时,哪些宏、源文件和编译选项可能改变分析器看到的代码?

16. 本节总结
#

静态分析器在编译期间读取代码并推断可能路径,不运行被分析程序。报告需要沿条件、返回值和使用点回溯,并与固定的版本、命令和源码一起保存。它能提供风险证据,但既不能证明程序正确,也不能替代主机测试或硬件验证。

17. 下一步
#

先完成实验并保留两次日志与散列,再用自己的话解释路径。课程生成本身不会提高掌握度,也不会修改 completed 或 mastery;后续主题由下一次计划决定。

18. 参考资料
#

  1. GNU Project, Static Analyzer Options (Using the GNU Compiler Collection (GCC)),GCC 15.2.0;访问日期:2026-09-28。用于核对 -fanalyzer、路径分析、误报/漏报边界及空指针诊断。[1]
  2. GNU Project, Warning Options (Using the GNU Compiler Collection (GCC)),GCC 15.2.0;访问日期:2026-09-28。用于核对普通告警、额外静态分析选项与 -Werror 的边界。[2]
  3. Arm, Arm Compiler for Embedded Reference Guide,Document ID 101754_0616_01_en,Version 6.16,Issue 01;访问日期:2026-09-28。仅作为迁移时核对 Arm 编译器自身命令行的第一方入口,不用于声称存在 GCC 分析器的等价功能。[3]
RuChen
作者
RuChen
EmbeddedStudy - 这篇文章属于一个选集。
§ 10: 本文