Polyspace静态分析配置实战:嵌入式代码安全与功能安全
2026/9/13 2:40:12 网站建设 项目流程

1. 为什么要搞懂 Polyspace 的项目配置

先说个背景。我在嵌入式行业摸爬滚打了十来年,做过汽车电子、工业控制、医疗器械,这些领域有一个共同点:代码出事不是蓝屏重启那么简单,是要出人命的。所以最近几年,静态代码分析工具在圈子里越来越普及,而 MathWorks 家的 Polyspace 是其中非常特别的一个。

为什么说特别?因为 Polyspace 不是那种抓抓拼写错误、规范问题的普通 lint 工具。它用的是形式化验证的底子,能在不给程序喂输入的情况下,证明你的 C/C++ 代码里有没有数组越界、除零、空指针解引用、整数溢出这类运行时错误。这就好比你请了一位极其严格的代码审计员,把每条代码路径都过了一遍,而且是数学意义上的“证明”,不是抽样、不是推测。

但工具再好,配置不对等于白搭。我见过太多团队兴冲冲装好 Polyspace,结果跑出来的结果要么全是误报没法看,要么漏报一堆问题,最后把工具扔在一边。为什么?绝大多数坑都出在项目配置阶段。Polyspace 不理解你的硬件环境、编译器行为、代码裁剪方式,它就只能瞎猜,猜错自然结果不准。

这篇文章把我的实操经验整理出来,围绕 Polyspace 项目配置从建工程、设环境、调参、跑分析、到看结果这条线走一遍。适合刚接触 Polyspace 的嵌入式工程师、功能安全认证的项目成员,以及被静态分析结果搞得焦头烂额的朋友。看完你至少能少踩一半的坑。

2. 项目配置的整体思路:从“懂业务”到“懂工具”

2.1 先搞明白 Polyspace 的两种分析模式

Polyspace 对你的代码做分析时,有两条完全不同的路线:Bug Finder 和 Code Prover。这两者的差异非常关键,直接决定了你应该怎么配置项目。

Bug Finder 是“浅层扫描”,速度非常快,适合找常见的缺陷模式。它不追求穷尽每一条路径,而是用启发式的方式把可疑点标出来。优点是快,集成到 CI 里都没问题;缺点是可能漏,而且会出一些比较虚的告警。

Code Prover 是“形式化证明”,它会做完整的可达性分析,证明某段代码在运行到某一行时是否必然(或绝对不可能)发生某种错误。跑 Code Prover 出来的结果分成几类:红色表示确实存在缺陷路径,橙色表示可能是缺陷路径,绿色表示证明安全,灰色则表示代码不可达(死代码)。这是通过数学方式证明出来的,不是猜的。

我自己的习惯是:日常迭代用 Bug Finder,快跑快改;关键模块、发布前的全量检查和功能安全认证,用 Code Prover 做深度验证。

了解了这两套模式,你就明白为什么项目配置如此重要。Code Prover 的分析精度很大程度上取决于你给它输入的环境约束。你没有告诉它temperature_sensor的范围是 0 到 1023,它就会按整个 int 范围去分析,然后告诉你“这地方可能有溢出”,虽然你的硬件根本不可能产生那么大的值。这就是误报的金矿,也是配置的意义所在。

2.2 配置工作的三条主线

结合我这些年的经验,Polyspace 项目配置不管项目多大多小,核心就三条线:

  • 第一条线:让工具正确识别你的代码,包括源文件、头文件路径、编译器类型、语言标准。
  • 第二条线:让工具理解你的运行环境,包括外部输入范围、硬件位宽、中断行为、被裁剪的库。
  • 第三条线:让结果符合你的交付需求,包括告警级别、规则集、注释和审查流程、报告格式。

这三条线互相制约。代码识别错了,环境约束再多也没意义;环境约束不完整,结果再好看也站不住脚。所以配置 Polyspace 项目的过程,本质上就是把你脑子里的“这个系统怎么跑”翻译成 Polyspace 听得懂的约束。

配置主线解决的核心问题常用配置入口
代码识别编译器和语言环境项目设置中的 Compiler Settings
环境约束外部输入与硬件行为Code Prover Options / Main 设置
输出交付缺陷规则与报告格式检查设置、报告导出选项

很多工程师最容易犯的错,是跳过第二条线直接跑分析。结果 Code Prover 跑出几百上千条告警,其中有大量是因为工具不知道某个变量的实际物理范围导致的“伪缺陷”。你以为自己在做静态分析,实际上是在帮工具补背景知识。磨刀不误砍柴工,先把环境说清楚,后面干净利落。

3. 实操:创建项目与建立编译环境

3.1 从零创建一个 Polyspace 项目

新版 Polyspace 是在 MATLAB 桌面环境下使用的,也可以在命令行里调用脚本。我一般推荐先用 Desktop UI 创建项目,先把配置跑通了,再沉淀成脚本做自动化。

打开 MATLAB,在 HOME 标签页点击 Polyspace 图标,或者直接在命令行输入:

polyspaceConfigureProject

这是 MathWorks 提供的一个图形化工具,全程引导你创建一个新的 Polyspace 项目。跟着向导走,你会碰到这样几个核心选择:

  • 一种方式是基于 Makefile: 工具自动解析你的 Makefile,提取编译器调用参数、源文件列表、头文件路径。对已有大型工程来说这是最省事的方式。
  • 另一种方式是基于 CMake: 新版 Polyspace 支持 CMake 集成,让你在构建目录里生成compile_commands.json,然后直接导入。
  • 还有一种方式是手动指定: 如果你的工程没有现成的构建脚本(比如你只是想快速扫描某个文件夹里的代码),可以手动把源文件和参数填进去。

我强烈建议:如果你的项目已经有 Makefile 或者 CMake,不要手动输入,用解析的方式。手输不仅慢,而且容易漏掉头文件路径,一旦漏了,Polyspace 就提示找不到头文件,然后对着一个头文件的宏定义一路报错下去,体验极差。

以 Makefile 解析为例,配置界面会让你指定构建命令。如果你平时是直接输make,那就填make。如果你想先 clean 再 build,可以填make clean && make。Polyspace 会拦截编译过程中真实的编译命令,把它转换成自己的分析模型。

这里有个很关键的小细节:Polyspace 解析构建过程时,不需要你真正把目标文件编译出来。它只是“监督”编译命令的格式和参数,然后自己拿着这些参数去做代码分析。所以就算你当前的编译器没装全,也不影响它提取参数。当然,为了后续分析时的预处理器能正确展开头文件,编译器路径和系统头文件最好还是齐整的。

3.2 编译器选择的三个坑

在配置界面里,有一个字段是选择编译器。这个字段看似不起眼,实际上坑特别多。

第一坑:不要把 GCC 配置成 ARM Compiler。Polyspace 内部用默认编译器参数去预处理器和解析代码,如果你告诉它用的是 GCC,它就会用 GNU 内联汇编的语法去解析你的__asm代码,而 ARM Compiler 的语法跟 GCC 不一样,结果就是一堆莫名其妙的语法错误。

第二坑:要考虑<stdint.h>等标准头的差异。嵌入式交叉编译器的标准头文件跟宿主编译器差异很大。如果你的编译器列表里找不到对应型号,最好指定成同类的基础类型,比如“GCC for ARM Embedded Processors”,然后手动调整后续的 include 路径。

第三坑:配置语言标准。编译器选项里的-std=c99还是-std=c11,会直接影响 Polyspace 对代码语义的理解。比如_Bool在 C99 里是内置类型,在 C89 里就不是。你代码里什么都没变,语言标准配置错了,Pre-Processing 阶段就会报错。

我的建议是:在创建项目阶段多花十分钟,把编译器类型和语言标准核对准确,这十分钟后面能帮你省十个小时的排查时间。反正我踩过的那些“莫名字法错误”,回头看八成都是编译器配错了。

3.3 头文件路径与宏定义配置

编译器解析完毕之后,Polyspace 会在项目设置里自动填入一大堆 Include Path 和 Define。这个时候不要全信,一定要在 Analyze 之前检查一遍(也可以配置一个 Initial Analysis Setup 步骤自动检查)。

怎么检查?在项目界面的 Project Browser 里,展开项目树,找到 Compiler Settings 相关节点,翻看头文件路径列表。确认以下两件事:

  • 所有项目内部的相对路径都正确解析。
  • 有实际条件编译的宏(比如STM32F407xxPRODUCTION_BUILD)在 Define 列表里。

宏定义这块我必须多说一句。真实工程里,条件编译几乎无处不在。比如:

#ifdef TEST_MODE buffer_size = 128; #else buffer_size = 256; #endif

如果不加TEST_MODE宏,Polyspace 默认走#else分支,分析的就是生产代码。如果你正好想测测试模式的代码,那就得在配置里加上这个宏。反过来说,如果生产代码和测试代码在逻辑上差别很大,而你两种都想覆盖,就得建两个不同的项目配置,分别定义不同的宏组合。

在我实际用过的项目里,最复杂的宏组合来自芯片厂商的 SDK。一个 Sensor 库头文件里可能嵌套了几百个宏分支,不同的片子配置不同的宏。没有在项目配置阶段把宏理清楚,后面根本没法看结果。这份功夫不能省。

4. 核心配置详解:把“运行环境”翻译给 Polyspace

4.1 设置外部输入的范围(Stub 与 Volatile 配置)

如果说编译器配置是基础,那么外部输入范围配置就是 Polyspace 配置的灵魂,尤其对 Code Prover。

嵌入式代码的很多运行时错误来自外设输入。ADC 读回来的值、CAN 总线收到的报文、按键的电平信号,这些值在 Polyspace 看来都是“自由变量”。自由变量意味着它的可能取值范围是整个 int/long 的范围。如果 Polyspace 按整个 int 范围分析一个char型变量,它当然能找出“可能溢出”的路径来。

但在真实世界里,8 位 ADC 的读数永远在 0 到 255 之间。如果我们不告诉 Polyspace 这一点,告警就不可避免。

配置方法有两种。

一种是通过polyspace选项设置入口映射。在 Code Prover 的配置面板里,找到Inputs相关选项。这里可以指定一个“输入配置函数”或“输入配置文件”,比如:

volatile unsigned char adc_val;

Polyspace 会允许你把某个全局变量标记为“外部输入”,并指定它的范围是[0, 255]

另一种方式是在代码上加注释指令。Polyspace 提供了一系列代码注释指令,比如:

/* polyspace<CALL> */ /* polyspace<DEFINE> */ #pragma Polyspace_Range("adc_val", 0, 255)

用注释指定某个变量或某个函数返回值的取值范围。这种方法不需要动业务逻辑代码,只是加注释,我个人比较推荐。因为注释不参与编译,对实际产物零影响,又能精确约束分析语义。

补充一个细节:如果你用的芯片厂商有官方的外设库,通常厂商会提供一个针对 Polyspace 的功能安全支持包,里面已经把这些外设寄存器的 Volatile 属性、位宽、地址区间都配好了。比如 ST 就发布过基于 Polyspace 的“High Integrity”配置套件。遇到这类支持包,优先使用,比自己手配高效太多了。

4.2 配置 Main 函数入口与启动代码

Code Prover 做全套证明时,需要一个分析的“起点”。默认情况下,Polyspace 会以main函数作为起点。但嵌入式代码的情况特殊,很多代码根本不在main里跑,而是被中断服务函数调用的,或者由一个超级循环调度器统一调度的。

如果你的代码依赖中断触发的函数,而这些函数并没有被main调用,那 Polyspace 默认只会分析main可达的路径。其他函数全部变成“灰色不可达”,等于白配。

怎么处理?Polyspace 提供了一个很实用的参数:-main选项,可以指定分析时使用哪个函数作为入口。你可以把入口指定为某一个具名函数,也可以用-entry-point的方式,在一个文件里列出所有需要作为分析入口的函数。

比如我有一次分析一个 CAN 中断处理程序,业务上是CAN_RX_ISR()从硬件寄存器读数据、调用process_can_message()更新控制逻辑。如果只分析main()process_can_message()就是一片灰色。配置时把CAN_RX_ISR也加进入口列表,分析范围立刻覆盖到了。

还有一种更“狠”的玩法:用 Polyspace 的自动生成 Main 功能。它可以根据你的配置自动生成一个抽象入口,把全局 volatile 变量都初始化成未知值,然后调用你指定的入口函数。这样既保留了入口函数的独立性,也帮工具设置了合理的输入初值。

4.3 处理动态内存与堆配置

嵌入式里有些项目干脆不用 malloc,但很多非硬实时模块会用。Polyspace 分析动态内存时,如果不知道堆的范围,就会把 malloc 的返回值当作“可能来自任意地址”,这时指针运算的告警会激增。

在 Code Prover 配置里,有一个Heap相关的设置项,可以指定堆大小。比如你项目的链接脚本里定义了堆区大小为0x1000,那你就把这个值告诉 Polyspace。这样它分析 malloc 返回指针的偏移时,会根据堆边界判断是否越界,而不是默认整个地址空间都可访问。

不过我也得说实话,动态内存配合形式化验证,在复杂场景下依然容易出“灰色区域”。很多功能安全标准(比如我接触过的 ISO 26262、IEC 61508)在最高 ASIL 等级下,甚至推荐禁用动态内存。如果你的项目对可靠性的要求极高,多考虑静态分配方案,这对 Polyspace 分析体验和实际代码质量都有好处。

4.4 第三方库与不分析代码的排除

大多数嵌入式项目会用到第三方库:通信协议栈、加密算法库、驱动库等。这些库的代码我们一般不修改,也不想让它污染自己的告警清单。Polyspace 支持把某个目录或某个文件标记为“不分析”或“按聚合方式分析”。

我常用的做法是:把第三方库目录加进-do-not-analyze列表,或者在项目界面的 Sources 节点上右键,选择排除。这样工具会保留这个文件里的函数声明供主分析使用,但不会深入函数内部做逐路径分析,跑起来更快、结果也更聚焦。

但有一个例外:如果第三方库是你的核心控制逻辑的关键支撑(比如 PID 算法库、状态机库),还是应该分析深一点。因为这些库里的错误会直接传导到你的应用代码里。我建议至少把“应用直接调用”的第三方函数加入深度分析名单,其他利用接口但内部不关心的,再排除也不迟。

另外,如果有些代码文件是有意保留给未来功能用的,当前版本根本不会编译进固件,也请从分析源文件列表中移除。Polyspace 分析一个文件时是按“它会被编译”来假设的,如果没有被 Makefile 选中却出现在源文件列表里,它也会照样分析,给出一堆跟当前发布版本毫无关系的告警。这类冗余影响很小,但累积起来就降低信噪比了。

5. 分析与结果解读:配置文件之外的关键操作

5.1 启动分析时你应该盯着的四个指标

配置完成后,点击 Analyze 按钮。Bug Finder 一般几分钟内出结果,Code Prover 时间会长,工程上大型项目跑几个小时都正常。分析运行期间,Polyspace 会有一个运行监视面板,我一般盯这么四个指标:

  • 编译阶段是否通过:是不是有文件解析失败了。
  • 是否出现了Polyspace<UNKNOWN>类异常:这通常意味着某个语句的语义不能被工具理解,可能是编译器特性或汇编代码导致。
  • 分析进度百分比:Code Prover 是按函数逐步推进的,如果某函数卡住,日志里通常会显示具体位置。
  • 内存占用和进程数:如果 OOM(内存不足),需要调节并发度或者关闭一些高开销检查。

如果你发现编译阶段就报错,那多半是语言标准、头文件路径或宏定义的问题。这时候返回第三章再去检查配置,不要硬着头皮继续分析,因为对错误代码做“分析”没有意义。

5.2 五种判定状态的含义

跑完之后,你会看到一大片带颜色的标注。别慌,先把五种状态搞清楚:

Polyspace 状态颜色含义
Definite红色该缺陷至少能通过一条可达路径发生
Possible橙色存在发生缺陷的必经步骤,但需要特定输入条件
Proved绿色数学上证明不会发生该缺陷
Dead Code灰色该代码在入口约束下不可达
Unreachable深灰该代码永远不可达

红色的处理优先级最高,但也别被一堆橙色吓到。橙色往往意味着你的约束还不够,要么你给它补充更准确的外部输入范围,要么代码里确实存在需要防御性编程处理的薄弱点。我处理橙色告警的习惯是:先快速浏览一遍,确认有没有逻辑硬伤,然后批量导出,交相关模块负责人判断。

绿色的意义常被忽视。实际上,Code Prover 能证明某类缺陷不会发生,本身就是一个很强的保障。功能安全认证里,管理者很看重这种“被证明无该缺陷”的证据。所以我建议在交付报告时,不仅列出缺陷清单,也把绿色证明的统计信息放进去,这是正面输出。

5.3 从“洪水般告警”到“可落地缺陷清单”的过滤技巧

大多数团队第一次跑 Polyspace 都会被告警数量吓到。我接过一个电机控制器项目,第一次全量 Code Prover 扫描,告警三千多条。当时团队直接要放弃这个工具。后来我们花了三天做配置优化,把告警压到了两百多条,其中真正需要改代码的只有三四十条。

优化手法主要有三个。

一是补充变量范围约束,把外设输入、传感器范围、协议字段长度用Polyspace_Range注释或者模拟模块配置进去,橙色告警大幅减少。

二是过滤已知误报模式。比如,有些外设寄存器是 volatile 的,读写顺序之间 Polyspace 会认为“任何时刻都可能被硬件改变”,从而导致一系列看似矛盾的告警。对这种,我建议在内核代码部分保留分析,但对外设寄存器访问层统一做“外部访问”标注,避免它在每个调用点重复告警。

三是按检查类型分类处理。Polyspace 里告警按检查类别(如“数值运算”“数组索引”“指针解引用”等)分类。先集中看高风险类别,如空指针和越界,再处理低风险类别,比如不影响到正确性的未使用变量告警。千万别大锅烩。

这里分享一个我觉得很有效的思路:把 Polyspace 当成评审会议上的“主动提问者”。它每次报一个可能性,就是问你“你有没有考虑过这种输入?”。如果你确认不会,就把原因用注释写进去,久而久之它就明白你的边界在哪里,误报越来越少。这不是工具变笨了,而是你的配置和代码已经把边界说清楚了。

6. 集成与自动化:把 Polyspace 配置固化到流程里

6.1 命令行与 CI 集成

当你的项目配置稳定下来之后,不要每次都打开 MATLAB 界面去点按钮了。Polyspace 提供了完整的命令行接口,你可以把配置导出成一个脚本或者用现有工程 XML 配置。

命令行调用示例:

polyspace-bug-finder -sources src/**/*.c -I include -compiler gnu-gcc -lang c -results-dir results

Code Prover 版本类似:

polyspace-code-prover -sources src/**/*.c -I include -compiler gnu-gcc -lang c -main main -results-dir results

注意:命令行模式下参数的语义跟 UI 里一一对应,建议你先在 UI 里跑通,再用-options-file导出参数,后续直接用这个 options 文件做 CI。

在 CI 流水线里,我一般这样组织阶段:提交代码 -> 静态编译检查 -> Bug Finder 快速扫描 ->(夜间)Code Prover 深度分析。Bug Finder 跑的比较快,可以挂在代码审查前;Code Prover 耗时长、产出重,适合定时任务,结果放到看板上供团队回溯。

Polyspace 还支持与 JUnit 报告格式集成,以及发布到 GitHub/GitLab 的 Code Scanning 格式,这样告警可以直接在 MR 的 Diff 上看到。对团队协作的效率提升非常明显。我实测下来,新成员看到告警直接出现在自己改的代码行上,比要求他们“自己打开工具看结果”要有效得多。

6.2 配置管理:让每一个人都在同一套约束下分析

最后聊一个组织层面的问题。

Polyspace 配置不是某一个人的私藏工具。如果公司里三个人各自建立项目,宏定义不同、入口函数不同、排除目录不同,那结果就没有可比性。我建议把项目配置文件(.psp文件或者生成的选项文件)纳入版本管理,和代码一起提交。

我在团队里定的规矩是:每个人的本地 Polyspace 项目都从共享配置派生,不允许私自改编译器类型、入口函数等关键参数。如果确实需要新增约束或调整范围,先更新工程级配置,再让大家同步。这样沉淀下来的分析基线才是可信的。

版本也需要注意。MathWorks 每年发布新版本,Polyspace 在不同版本之间分析器行为可能变化。如果团队同时有人用 R2022b、有人用 R2023a,哪怕是同一份配置,跑出来的结果也可能略有差异。最好统一版本,或者在报告里明确标注使用的工具版本。

7. 排错速查:我踩过的配置坑与解决记录

7.1 常用配置问题速查表

现象本质原因快速解法
大量红色“变量未初始化”告警编译器/入口配置错误导致分析起点异常核对-main与启动代码入口
整个文件灰色不可达文件未被入口函数调用或源文件列表多余清理源文件列表,或加入入口函数
头文件中排山倒海的类型错误语言标准或编译器类型不匹配检查编译器选项和语言标准
大量“可能溢出”但实际不可能外部输入范围未约束Polyspace_Range补充输入范围
找不到标准头文件include 路径设置遗漏检查 Compiler Settings 中的系统头文件路径
Bug Finder 快但漏,Code Prover 慢但全两者本身定位不同分类使用:CI 用 Bug Finder,认证用 Code Prover
告警清单里混入第三方库告警未设置排除目录在源文件节点排除或加-do-not-analyze

7.2 一个让我印象深刻的踩坑案例

有一次我分析一个呼吸机控制板的代码,这个板子用的是某厂家的 M4 内核 MCU。工程是从 IAR 移植到 GCC 工具链的,代码里用了大量__attribute__和 IAR 风格的内建函数。

我配置时偷了个懒,编译器直接选了默认的 Desktop GCC,结果 Polyspace 分析出来的结果惨不忍睹:两百多处语法错误集中在头文件里的位域定义上。我花了两个小时逐一排查,最后发现是 IAR 的某些扩展语法在桌面 GCC 的语义下不成立,而 Polyspace 用它内置的解析器根本没法正确理解那些位域。

修正方式是:把编译器类型明确设为“ARM Compiler / GCC for ARM”,然后在预处理器宏里补上__GNUC__等平台宏。改完再跑,语法错误清零,分析结果立马变得干净。

这个案例我想强调的是:配置信息绝不只是带个“形式”,它会实质性地影响 Polyspace 解析代码的方式。你把运行环境描述得越准确,它的预处理器和分析器就越能贴近真实的编译过程。偷懒一时爽,排错火葬场,说的就是这个。

7.3 排错方法论:三步定位法

如果你碰到一个 Polyspace 相关的怪问题,我推荐一套三步定位法。

第一步,先检查预处理结果。Polyspace 界面里可以查看“预处理后的文件”,确认宏是否按预期展开,头文件是否按预期包含。这一步能筛掉八成与配置相关的问题。

第二步,看日志里有没有Parsing StageCompilation Stage的报错。编译阶段没过,之后分析阶段的行为都不可信。先解决编译阶段的报错,再谈后面的分析结果。

第三步,简化复现。如果你怀疑某个特定函数或文件导致异常,临时建一个只包含该文件的配置,跑通后再逐步加回其他文件。二分定位法在这种场景下很高效。

这套方法论我已经在多个项目里验证过,每次都能把排查时间缩短到半小时以内。建议收藏备用。

8. 最后再分享一点个人经验

如果你是从零开始接触 Polyspace,我的建议是先别急着追求“零告警”。

Polyspace 更像一面放大镜,它会把代码里所有未定义的行为和潜在风险放大给你看。这种可能性的暴露在一开始往往让人难以接受。但这个过程是必经的。你花在配置上的每一分钟,都会在后面减少十分钟的无效告警排查。

还有一点,不要迷信工具,也不要全盘否定工具。Polyspace 有它很强的能力,也有它理解不了的场景,比如高耦合的多任务并发访问、复杂的动态调度策略。在这些场景下,它给出的结果只是“参考意见”,最终决策还是得靠人来判断。工具是把你的经验和判断力放大了,而不是替代了它。

我这些年配置 Polyspace 关注的最重要的一件事:配置不是技术问题,而是“怎么把自己的领域知识结构化地表达给工具”的问题。一旦你建立起这个认知,遇到再复杂的配置需求,你都能找到答案。

希望这些内容对你有用。如果你在配置过程中碰到什么绕不过去的怪问题,欢迎在评论区留言,我会尽量回复。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询