简介:《Polyspace工具使用说明》面向嵌入式软件开发者、测试与代码审核人员,围绕C/C++代码静态分析,帮助排查编码错误、规则一致性(如MISRA/JSF)、运行时风险、静态度量与测试覆盖度等问题。压缩包内共1个PDF文件,约828KB,篇幅集中于工具认知与结果判读,便于随查随用;已有2879人学习或下载。内容涵盖工程建立、代码导入、分析模块选择、分析项配置与检查运行,说明Bug Finder与Code Prover的差异,并借Dashboard、Result List、Source、Result Details等窗口解读风险分布、问题定位、死代码与潜在溢出示例,以及绿、红、橙、灰四级代码安全含义,适合作为嵌入式静态分析入门与日常查阅参考。
1. Polyspace 是什么:一段两百行的 C 代码,能藏多少个运行时坑
有个做嵌入式软件的朋友,把一段带signed char运算和外部函数调用的 C 代码贴进工程,本地编译零警告,台架跑了一周也正常,就签字放行了。三个月后现场偶发复位,日志里只剩一条除零记录。回头再看,问题不在逻辑写错,而在变量取值范围、外部函数返回值、编译器类型宽度这些“编译器不替你算、单元测试也未必覆盖”的地方。Polyspace 就是干这件事的:面向嵌入式软件的静态分析工具,对 C/C++ 代码做代码分析,把可能溢出、可能除零、永远走不到的死代码这类运行时风险提前标出来。它适合两类人:一类是写驱动、写控制逻辑、要过代码规范评审的一线开发者;另一类是接手存量代码、想知道“这堆东西到底安不安全”的维护者。工具本身不神奇,神奇的是它把“靠运气”的部分变成了有颜色的结论。
2. 工程建立与代码导入:五步搭出一个能复现的分析工程
2.1 先定编译器抽象,再谈分析结果
很多人第一次用 Polyspace,打开就点 New Project,把.c文件拖进去,直接 Run,然后对着结果发懵。问题往往出在最前面一步:没告诉工具“这段代码最终跑在什么编译器上”。静态分析要算变量的取值范围,而char到底有没有符号、int是 16 位还是 32 位、short和指针的宽度、有没有 DSP 的扩展累加器,这些直接决定溢出判断的结论。默认配置给的是一套通用抽象,放在桌面环境里没问题,放到某些 16 位 MCU 上就会算出完全不同的结果。
我一般的顺序是:先确认目标编译器和处理器型号,在 Configuration 里把 target 选对;如果列表里没有完全匹配的,就手工指定类型宽度和char的符号性。这一步花十分钟,后面能省掉一堆“这明明是误报”的争论。注意,target 选错不会让分析失败,它只会安静地给你一份看起来很专业的错误结论,这种坑最难发现。
判断 target 是否选对,有个土办法:挑一个你确定会溢出和确定不会溢出的函数各跑一遍,看结论是否和你手算的一致。对不上,回去查类型宽度。
2.2 五步建工程:从 New Project 到 Run
整个流程在图形界面里是五个动作,但每一步都有输入项和对应的失败模式。下面这张表是我自己整理的操作对照,照着填基本不会卡住。
| 步骤 | 操作 | 关键输入 | 常见失败 |
|---|---|---|---|
| 1 | 新建工程 | 工程名、存放路径 | 路径含中文或空格,后续命令行调用失败 |
| 2 | 导入源文件 | Project Source Files | 只加了.c没加.h所在目录,编译阶段报找不到头文件 |
| 3 | 配置包含路径 | Project Include Folders | 相对路径写错,换机器就崩 |
| 4 | 选择分析模块 | Module Source Files | 模块里混入了测试桩文件,结论被污染 |
| 5 | 配置并运行 | Configuration + Run | 默认配置直接跑,规则集和实际评审要求不一致 |
源文件目录我习惯这样组织,工程文件、源码、头文件、结果输出各占一层,换机器时整包拷走就能复现:
poly_demo/ ├── prj/ # 工程文件与配置 ├── src/ │ └── fun_test.c # 待分析源文件 ├── inc/ # 头文件目录,本例为空 └── results/ # 分析结果输出第 2 步导入的源文件就是下面这段,后面所有结果解读都围绕它展开,建议先原样建一份:
/* fun_test.c —— 用于演示的示例源函数 */ extern signed char extFunc(signed char y); /* 外部函数,实现不可见 */ signed char firstfun(void) { signed char x = 1; signed char y; signed char z; signed char res; y = x + 5; /* x 已知为 1,y 恒为 6 */ if (y < 0) { y = 0; /* 该分支永远不成立 */ } z = extFunc(y); /* 返回值范围未知,取决于外部实现 */ if (z <= 123) { res = z + 5; /* z 取 123 时,res 达 128,越过 signed char 上限 */ } else { res = z + 6; /* z 取 127 时,res 达 133,同样溢出 */ res = res / 2; } return res; }这段代码是整篇的“标本”。它只有十九行,却同时埋了死代码、整数溢出、外部输入不确定三类问题,正好把 Bug Finder 和 Code Prover 的差异摆到台面上。导入时把fun_test.c放进 Project Source Files,头文件目录留空即可,因为extFunc只有声明没有实现,工具会按外部符号处理。
第 4 步的 Module 选择容易被忽略。Module 决定“哪些文件一起分析”,如果工程里同时放了产品代码和单元测试桩,把桩文件选进同一个 Module,外部函数的返回值就会被桩的实现“定死”,Code Prover 算出来的区间偏乐观,结论就不可信了。常见做法是产品和测试桩分两个 Module,各自出结果。
2.3 Configuration 里真正影响结果的四个参数
Configuration 面板项目很多,真正会改变结论的其实就四个,其余保持默认即可。
第一个是目标编译器抽象,前面已经说过,选错等于全盘错。第二个是输入范围约束,也就是告诉工具“哪些全局变量、哪些函数参数有确定的取值范围”。默认情况下,外部函数的返回值被视为全范围,signed char就是 -128 到 127,这会让下游所有运算都显得“可能溢出”。如果你的接口文档里写明了extFunc只返回 0 到 100,把这个约束填进去,橙色警告会大面积消失。第三个是规则集,MISRA C:2012、JSF++ 这类规范是按条目组织的,可以全开也可以只开项目要求的那部分,全开会带来大量存量代码告警,反而不利于推进。第四个是 Code Prover 的验证深度,级别越高,分析越彻底、耗时越长,日常回归用中等深度就够。
提示:输入范围约束不是“消警告”的开关,它的本质是把接口契约写进分析模型。填之前先确认契约是真的,否则等于自己骗自己。
3. Bug Finder 与 Code Prover 的分工:规则扫描和运行时验证别混着用
3.1 Bug Finder:把代码规范和缺陷模式一次性扫出来
Bug Finder 走的是模式匹配加规则检查的路子:一边按 MISRA、JSF++、CWE 这些条目逐条核对编码规范,一边识别一批已知的缺陷模式——数组越界、空指针解引用、未初始化变量、不可达代码、整数溢出、除零、内存泄漏。它的特点是快,一个中等规模模块几分钟就能出结果,适合塞进每次提交的检查环节。
拿示例函数跑一遍,Bug Finder 会给出两类结论:y < 0这个分支永远不成立,属于死代码;res = z + 5和res = z + 6两处存在整数溢出风险。前者是控制流分析的结果,后者是类型范围分析的结果。它不会告诉你“这条路径在什么输入下会真的溢出”,那是 Code Prover 的活。
Bug Finder 的结果里,规则类告警和缺陷类告警要分开看。规则告警是“写法不符合规范”,缺陷告警是“这里可能真的出事”。存量代码第一次接入,规则告警动辄上千条,这时候的正确做法是先只开缺陷类检查,把真问题清掉,再逐步加规则,而不是一上来就全开然后被淹没。
3.2 Code Prover:用抽象解释把变量取值范围算到底
Code Prover 的方法不一样。它把每个变量表示成一个区间,沿着所有可能的执行路径往下推,遇到分支就分裂成两个状态,遇到循环就做不动点迭代,最后给每条语句一个颜色结论。因为它是“对所有路径都算”,所以结论是保守的:只要有一条路径可能出问题,就标出来,宁可多报也不漏报。
这种保守性带来两个直接后果。一是外部输入不确定时,橙色会很多,因为工具必须假设最坏情况;二是绿色才真正值钱,绿色意味着“在这个抽象模型下,所有路径都安全”,是一份可以拿去做评审的证据。
示例函数在 Code Prover 下的推演过程大致是这样:x = 1是确定值,y = x + 5推出y = 6,if (y < 0)的两个分支里只有一个可达,另一条判为不可达,显示灰色。z = extFunc(y)因为函数体不可见,z被赋予signed char的全范围 -128 到 127。进入if (z <= 123),真分支里z是 -128 到 123,res = z + 5最大到 128,越过 127,标橙色;假分支里z是 124 到 127,res = z + 6是 130 到 133,同样越界,也标橙色。如果extFunc的返回值范围被约束成 0 到 100,这两处橙色会直接变绿,这就是约束的价值。
3.3 什么时候只跑一个,什么时候两个都跑
选型上没必要二选一,两者是互补关系,关键是排进不同的节奏。
日常提交、每次合并请求,跑 Bug Finder 就够了。它快,能挡住“新写的代码引入明显缺陷”这一类问题,反馈及时,开发者不会因为等结果而绕过检查。版本里程碑、发布前回归,跑 Code Prover。它慢,但能给出“这段代码在所有可能的输入下都不会溢出/不会除零”的结论,这是安全相关评审真正需要的东西。
新模块第一次接入,我的顺序是反过来的:先跑 Code Prover,把输入范围约束梳理清楚,因为梳理约束的过程本质上是在补接口文档;约束定下来之后再跑 Bug Finder 做规则收敛。顺序反了,先被上千条规则告警缠住,反而没精力去处理真正危险的运行时问题。
还有一种情况只跑 Bug Finder:接手一个几十万行的存量工程,第一步是摸清家底。这时候全量 Code Prover 可能跑上一整天,先用 Bug Finder 按文件排序,挑告警密度最高的几个文件先看,投入产出比更高。
3.4 命令行批处理:让分析脱离鼠标
图形界面适合调参和看结果,批量回归还是要靠命令行。Polyspace 提供polyspace-bug-finder和polyspace-code-prover两个入口,基本用法如下:
# Bug Finder 批处理:指定源文件、包含目录、结果输出目录 polyspace-bug-finder \ -sources src/fun_test.c \ -include inc \ -results-dir results/bf \ -misra-c 2012 # Code Prover 批处理:同样的输入,换分析引擎 polyspace-code-prover \ -sources src/fun_test.c \ -include inc \ -results-dir results/cp \ -target x86_64-sources后面可以跟多个文件,也可以给一个文件列表;-include对应工程里的 Include Folders;-results-dir是结果落盘位置,后续可以用polyspace-results-export导成 CSV 做趋势统计。-misra-c 2012指定规则集版本,如果你只关心缺陷不关心规范,把这个参数去掉能显著缩短时间。-target指定编译器抽象,等价于界面里的 target 选择,换平台时必须改。
注意:不同安装版本里命令行选项名可能有细微差异,第一次用之前先跑
-help确认一遍,别直接抄别人的脚本。
批处理真正的价值在于可重复。界面里点出来的配置存在工程文件里,换个人、换台机器就可能不一样;命令行脚本进版本库,谁跑都是同一份配置,结果才有可比性。
4. 结果窗口逐层拆解:三张图、一个列表、一段执行路径
4.1 Dashboard 的三张图先看哪张
分析跑完,Dashboard 会摆出三张图:风险数量饼图、覆盖度柱状图、错误类型分布图。很多人的习惯是先看饼图里红色占多少,这个顺序其实反了。先看覆盖度。
覆盖度的含义是“实际参与分析的代码占应分析代码的比例”。如果这个数字明显偏低,说明有文件没编译进去、有头文件没找到、有模块没选全,这时候饼图上的红绿分布毫无意义,因为一大块代码根本没被检查。我在一个项目里见过覆盖度只有百分之六十几的结果,原因是一个条件编译宏没打开,一半的驱动代码被#ifdef掉了,工具很诚实地告诉你“我没看那些”,但看图的人只盯着红色比例。
覆盖度确认没问题,再看错误类型分布,它能告诉你这个模块的主要风险集中在哪一类:是空指针多,还是溢出多,还是规则违例多。类型分布决定后面的处理策略,集中在一类问题上,往往是一个公共函数或者一套编码习惯导致的,改一处能消一片。
最后才看饼图的比例。红色高风险的数量是给管理层看的指标,工程师更该关心的是“每一条红色对应哪一行代码、触发条件是什么”。
4.2 Result List 到 Source 的联动定位
Result List 是结果的中枢,按规则类型、文件、严重程度都能分组。双击某一条,Source 窗口自动跳到对应行,这是最高频的操作。
Source 窗口里有三层信息。第一层是行号左侧的标记,红圈表示风险位置,灰圈表示不可达代码。第二层是代码染色,红色是高危,橙色是取决于外部条件的中风险,绿色是通过验证的安全代码,灰色是不会执行到的死代码。第三层最容易被忽略:被染色的表达式下方会有一排小圆点,鼠标悬停上去,弹窗会说明“这个结论是怎么来的”,比如“变量 z 的取值范围是 -128 到 127,加 5 后超出 signed char 上限”。这排小圆点是整个工具里信息密度最高的地方,看懂了它,很多“这肯定是误报”的疑问自己就消了。
示例函数里y = x + 5这行会是绿色,if (y < 0)的分支体是灰色,res = z + 5是橙色,这三处挨在一起,正好演示了四种颜色中的三种。把鼠标停在橙色那行的圆点上,弹窗会明确写出区间推导过程。
4.3 Result Details 里的执行路径怎么读
Result Details 展示的是“工具认为会出问题的那条路径”。它按步骤列出每个变量在每一步的取值区间,以及走了哪个分支。这份信息在写缺陷报告时特别有用,因为你不能只写“第 14 行可能溢出”,得写清楚“当外部函数返回 123 时,加 5 后得到 128,超出 signed char 上限,会导致未定义行为”。
读这份路径有个技巧:从结论往上倒着看。先看最后一步的变量区间,再看它是怎么被前一步推出来的,一路倒推到最初的输入假设。如果倒推到最后发现假设本身不成立(比如某个全局变量在初始化函数里已经被限定在 0 到 10),那就是约束没填,回去补约束重跑,比逐条争论误报高效得多。
4.4 拿示例函数把四种颜色走一遍
把前面的推演整理成一张对照表,跑完结果可以拿来核对,对不上说明 target 或约束配错了。
| 代码位置 | 预期颜色 | 判定依据 |
|---|---|---|
y = x + 5 | 绿色 | x 确定为 1,y 确定为 6,无溢出 |
if (y < 0)分支体 | 灰色 | y 恒为 6,条件恒假,分支不可达 |
z = extFunc(y) | 橙色 | 外部函数返回值范围未知,取全范围 |
res = z + 5 | 橙色 | z 上限 123 时 res 达 128,越过上限 |
res = z + 6 | 橙色 | z 下限 124 时 res 达 130,同样越界 |
res = res / 2 | 橙色 | 除数固定为 2,无除零风险,但入参已溢出 |
这张表的意义在于建立校准习惯。每次换 target、换约束、升级工具版本,先拿这段标本跑一遍,颜色对得上,再去分析真实工程。静态分析工具最怕的就是“配置悄悄变了但没人发现”,一份固定的回归标本能把这个风险摁住。
5. 避坑与排查:分析跑不起来、满屏红、结果对不上怎么办
5.1 编译阶段就红:头文件路径和编译器抽象没对上
现象:点 Run 之后 Output Summary 里刷出一片红色告警,提示找不到某个.h,分析直接中断,覆盖度接近零。
原因:Project Include Folders 里只填了源码目录,没填头文件目录;或者路径用了绝对路径,换台机器就失效;还有一种隐蔽情况是条件编译宏没定义,代码走了另一条#ifdef分支,引用了不同的头文件。
解决:包含路径一律用相对工程文件的相对路径,跟着工程一起进版本库;条件编译宏在 Configuration 的预定义宏里显式列出,别指望工具自动推断。改完先看 Output Summary 里编译是否干净,再谈结果。
5.2 满屏高风险:输入范围没约束,全是保守估计
现象:一个几百行的模块跑出上百条红色和橙色,逐条看下去发现全是“外部函数返回值可能导致溢出”“全局变量范围未知”。
原因:Code Prover 的默认假设是最坏情况,所有外部输入都取全范围。这不是误报,是信息缺失——工具不知道你的接口契约。
解决:把接口文档里的取值范围填进约束,或者用-data-range之类的选项在命令行里声明。填完之后橙色通常会大幅减少,剩下的才是真问题。这一步的本质是把“工具猜”变成“你告诉它”,不是消警告。
5.3 全绿不等于安全:未覆盖代码和未定义行为
现象:Dashboard 一片绿色,交付评审时被问“凭什么说安全”,答不上来。
原因:绿色只代表“在已分析的代码、已建模的假设下没有发现问题”。未参与分析的代码、被条件编译裁掉的分支、超出建模能力的语言特性(比如某些内联汇编、函数指针的间接调用),都不在绿色的覆盖范围内。
解决:交付前把覆盖度、未分析文件清单、函数指针调用点清单一起导出来,作为结论的边界说明。绿色是证据,但证据有适用范围,说清楚范围比喊“全绿”有说服力。
5.4 结果和上一次对不上:增量分析和缓存
现象:代码只改了一行,重跑之后告警数量从 30 条变成 80 条,或者反过来少了一半。
原因:增量分析复用了上次的结果缓存,而被修改的文件影响的调用链比想象中长;或者两次跑的 Configuration 不一样(有人改了规则集没同步);还有一种是结果目录没清干净,新旧结果混在一起统计。
解决:里程碑回归一律全量跑,结果目录先清空;日常增量跑的时候,把配置文件的哈希值和结果一起存,对不上就重跑。这个习惯看着笨,但能省掉大量“到底哪个结论是真的”的扯皮。
5.5 规则集与存量代码打架:MISRA 开关怎么取舍
现象:第一次给存量工程开 MISRA C:2012,告警几千条,团队直接失去信心,工具被弃用。
原因:存量代码本来就不是按这套规范写的,全量开启等于拿新标准审判十年前的设计,整改成本远超收益。
解决:分三步走。第一步只开缺陷类检查,清掉真正的运行时风险;第二步按文件圈定新代码范围,只对新增和重大修改的文件开规则检查,把规范挡在入口;第三步对存量代码做规则告警统计,挑出高频条目集中整改,一次改一类。规范落地的关键从来不是工具开多少开关,而是新代码不再产生新债。
6. 进阶落地:把分析结果接进日常提交和趋势看板
工具用熟之后,真正拉开差距的不是会不会点按钮,而是能不能让分析结果持续产生价值。我一般做三件事。
第一件,给关键接口补约束。把 Code Prover 的橙色当成“契约缺失清单”,每消掉一片橙色,就意味着一份接口取值范围被写进了配置或者文档。这件事做上几个版本,橙色会稳定在一个很低的水平,剩下的每一条都值得单独开会讨论。约束可以用命令行参数批量维护:
# 把接口契约写进分析:声明外部函数的返回值范围 polyspace-code-prover \ -sources src/fun_test.c \ -include inc \ -results-dir results/cp \ -data-range "extFunc:[-128,100]"-data-range的写法是按符号名给区间,多个符号可以重复给。填之前务必和接口负责人确认,猜出来的范围比不填更危险。
第二件,把结果导成结构化数据看趋势。每次回归把结果导出成 CSV,按规则类型和文件聚合,画一条随版本变化的曲线:
# 分析结果导出为 CSV,便于做版本间对比 polyspace-results-export \ -results-dir results/cp \ -format csv \ -output results/cp/report.csv导出的表里至少保留规则编号、文件、行号、严重级别四列。有了这份数据,评审时就不用再争论“这个版本质量到底有没有变好”,曲线摆在那里。
第三件,把分析卡在合并请求上。规则很简单:新增或修改的文件,缺陷类告警数必须为零;规则类告警允许有,但总数不能比基线高。这条卡点一旦立起来,规范就从“事后评审”变成了“事前拦截”,团队对工具的抵触也会明显下降,因为大家发现它拦住的确实是自己不想背的锅。
提示:卡点阈值不要一次定死,先观察两三个版本的实际分布再定,定得太严会逼着大家想办法绕过检查。
有个教训我印象很深。早些年我把 Code Prover 全绿当成发布门槛,结果团队为了凑绿色,把大量外部输入约束填成了“实际测试见过的范围”,曲线是好看了,一次现场异常输入还是把系统打挂了。从那以后我每次配约束,都强制要求填的人写清楚依据来源,是接口文档、是芯片手册、还是实测统计,没有依据的一律按全范围处理。宁可橙色多几条,也不要用假前提换假安心。
希望这些步骤和坑能帮你少走点弯路,把这份工具说明真正用成手上的活。
本文还有配套的精品资源,点击获取