☰
Polyspace C/C++静态分析:双组件与四色判读避坑
2026/10/31 9:22:27 网站建设 项目流程

简介:《Polyspace工具使用说明》面向嵌入式软件开发者、测试与代码审核人员,是一份聚焦C/C++静态分析的入门资料,主要帮助排查代码鲁棒性、规则一致性和运行时风险。压缩包内仅含1个PDF文件,约828KB,内容集中,便于随查随用。文档介绍Polyspace在编码错误检测、MISRA/JSF规则检查、静态度量与测试覆盖度方面的作用,并说明新建工程、导入源码、选择分析模块、配置检查项和运行分析的流程;同时梳理Dashboard、Result List、Source等结果窗口,解释风险数量、覆盖度、错误类型统计及代码定位与原因提示,并区分Bug Finder静态检测与Code Prover深层验证的侧重点。已有2889人学习,适合希望快速建立工具使用框架、理解结果判读并用于嵌入式软件质量保障的读者。

1. 从一段能编译通过的函数说起:Polyspace 抓的是编译器不管的那类错

上周有同事拿着一份fun_test.c来找我,说这段代码在三个编译器上都能编过,-Wall -Wextra也没吭声,但客户现场偶发返回值异常。我把它丢进 Polyspace 跑了一遍,两分钟出结果:一处永远走不到的死代码、两处 signed char 溢出风险,全是编译器不会主动提示的类型。这就是嵌入式软件静态分析的价值所在——它不做语法检查,做的是代码分析层面的推演,把「编译能过、跑起来炸」的路径提前摆到桌面上。Polyspace 面向 C/C++,分两个组件:Bug Finder 做快速静态检测,查编码规则、缺陷模式;Code Prover 做全路径抽象解释,查运行时错误。前者几分钟,后者可能跑几小时,分工明确。如果你在做车规、工控、医疗这类对代码鲁棒性有硬要求的项目,或者正被 MISRA 规则一致性审得头大,这套东西值得花半天摸熟。

2. 建工程与导入源码:fun_test.c 进分析流水线的四个配置位

2.1 先选对分析器:Bug Finder 与 Code Prover 的分工

新手最容易犯的错是直接上 Code Prover,等了四十分钟看到满屏橙色,然后不知道该干嘛。这两者的定位完全不同,先看清边界再动手。

维度Bug FinderCode Prover
分析方式模式匹配 + 局部数据流,不做全路径枚举抽象解释,对每个变量维护取值区间,路径敏感
回答的问题这段代码违反了哪条规则、有哪类已知缺陷模式这个变量在所有可能输入下会不会溢出、越界、除零
典型耗时几秒到几分钟几分钟到数小时,取决于函数规模与循环结构
结果性质确定性告警,基本可以直接改带可信度的判定,绿/红/橙/灰四色分级
适用阶段提交前、每日构建里程碑节点、发版前、组件级深度验证

我一般的顺序是:Bug Finder 先跑,把明显的越界、未初始化、规则违规清干净,代码质量抬到一个基准线;再用 Code Prover 做深层验证,重点盯橙色项,因为橙色恰恰是「取决于外部输入」的那部分,也是现场最容易翻车的地方。反过来先跑 Code Prover,你会被大量由低级缺陷衍生的橙色淹没,判读成本极高。

2.2 工程建立与源码导入:三个位置别填错

新建工程后进入导入环节,界面上有三个关键位置,填错任何一个都会导致分析结果失真甚至直接失败:

配置位置该填什么填错的典型后果
Project Source Files待分析的.c/.cpp源文件文件漏进来,覆盖率柱状图虚高,你以为查全了其实没查
Project Include Folders源文件#include的头文件搜索路径Output Summary 里出现黄色的「文件查找失败」,大量类型被当作未知
Module Source Files本次真正参与分析的文件子集全部导入但只想分析单个函数时,分析时间成倍膨胀

第三项容易被忽略。工程里挂 200 个文件、本次只想验证一个模块,就要在 Module 里做减法,只保留目标文件和它直接依赖的实现。头文件放 Include Folders,不要塞进 Source Files,否则会被当成编译单元重复分析。

拿来练手的源文件就是下面这段,把它存成fun_test.c:

/* fun_test.c —— 用于验证 Polyspace 基本分析流程的最小样例 */ 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; /* y 恒为 6,后续 y < 0 分支不可达 */ if( y < 0 ){ y = 0; } z = extFunc( y ); /* z 的取值完全由外部函数决定,分析器无从得知 */ if( z <= 123 ){ res = z + 5; /* z 取 123 时,128 超出 signed char 上限 */ }else{ res = z + 6; /* z 取 127 时,133 同样溢出 */ res = res / 2; } return res; }

这段代码有三个刻意埋下的看点。第一,signed char是 8 位有符号窄整型,取值范围 -128 到 127,任何超过 127 的中间结果都是未定义行为,而大多数编译器在默认优化级别下不会对此报警。第二,extFunc只有声明没有实现,分析器无法推断返回范围,只能按全区间处理,这正是 Code Prover 橙色结果的来源。第三,if( y < 0 )分支在y = x + 5 = 6的前提下永不可达,构成死代码。行数和逻辑都够短,出结果快,适合反复试配置。

2.3 编译配置与分析运行:让分析器看到和真实工具链一样的代码

Configuration 这一层决定分析器「怎么理解你的代码」,默认配置能跑通流程,但真要出可信结论,必须和实际编译选项对齐。几个必须核对的项:

  • 语言标准:C90 / C99 / C11 影响声明位置、隐式转换规则的判定,选错会凭空多出一批规则违规。
  • plain char 是否有符号:这是血泪项。char在某些目标平台上默认无符号,如果你的分析配置和实际工具链相反,所有基于char的溢出判定全部反转,绿变红、红变绿。
  • 预处理器宏定义:条件编译块里的代码,宏没给对就根本进不了分析范围,覆盖率会莫名其妙掉一截。
  • 编译器与目标平台:决定整型宽度、字节序、对齐方式,间接影响指针越界和类型转换类告警。

图形界面点「运行」是最直接的方式,勾选生成新结果文件,避免覆盖上一次的基线。需要挂到流水线上时,走命令行入口,写法大致如下:

# 命令行方式跑 Bug Finder;可执行文件名随版本略有差异, # 以安装目录 bin 下的实际文件为准,先用 -help 确认参数拼写 polyspace-bug-finder \ -sources fun_test.c \ -lang c \ -compiler gcc \ -target x86_64 \ -results-dir ./ps_results_bf

参数逐个说清楚:-sources后面跟参与分析的文件列表,多个文件用空格分隔,也支持响应文件批量传入;-lang指定语言,C 和 C++ 的分析规则集不同,别混着来;-compiler告诉分析器按哪套编译器的语义做预处理和类型推导;-target决定数据模型,32 位和 64 位平台上long的宽度不一样,溢出判定结果自然不同;-results-dir是结果落盘目录,建议按日期或提交号命名,方便回溯对比。第一次用某条命令前,先跑一遍-help把参数名核对清楚,不同版本的选项拼写会有微调,照抄网上教程经常翻车。

运行过程中 Output Summary 窗口是唯一的观察点。编译阶段有问题会出红色告警,头文件找不到是黄色告警,这两种情况都别急着看结果——分析器连代码都没完整理解,后面的结论没有参考价值。等进度条走完、状态变成分析结束,再进结果窗口。

3. Bug Finder 结果怎么读:Dashboard、Result List、Source 三窗口联动

3.1 Output Summary:先确认这次分析是不是有效

结果窗口有五个,但判读顺序不能乱。第一眼永远看 Output Summary,它记录的是分析过程本身是否健康。这里要区分两类信息:红色告警意味着编译环节就出问题了,比如宏展开失败、语法解析中断,这种情况下后续结果基本不可信,必须回去修配置;黄色告警是文件查找失败一类的降级提示,分析继续跑了,但某些类型被当成未知处理,可能产生误报也可能漏报。我见过最典型的场景是头文件路径少配了一级,Output Summary 里刷了十几条黄色,Dashboard 上覆盖率只有 40%,新手看到「覆盖度低」以为代码有问题,实际上是配置没给全。

分析正常结束时,这个窗口还会给出参与分析的文件数、函数数、总代码行数。拿它和你的工程实际规模对一下,数字对不上就说明有文件没进来,后面的所有结论都要打问号。

3.2 Dashboard:三个图先看覆盖度,再看饼图

Dashboard 是结果的总览,上面三张图信息密度不同,判读优先级也不一样。

风险饼图用颜色区分严重程度,红色是高风险,粉红色是低风险,点击任一扇区,Result List 会自动过滤出对应条目。这个图适合给管理层看,不适合给工程师看——它只告诉你「有几条」,不告诉你「是什么」。

分析覆盖度柱状图才是工程师该先看的。覆盖率低通常有三个原因:代码里有大量条件编译分支没被激活、某些函数因为缺少定义被跳过、分析配置把部分文件排除了。覆盖率不到七八成,饼图上的数字再漂亮也没有意义,因为没被覆盖的部分完全没查。

错误类型及数量图按缺陷类别聚合,比如数值溢出、不可达代码、未初始化变量各多少条。这张图的价值在于定位「这批代码的主要问题集中在哪一类」,如果溢出类占了七成,那多半是整型宽度选型或者类型转换的系统性问题,不是个别函数的偶发失误,改法也应该从类型定义层入手,而不是逐个函数打补丁。

3.3 Result List 与 Source:红圈、红代码、小圆点三层定位

Result List 把每条风险按类型、位置、数量列出来。以样例代码为例,跑完能看到一条「不可达代码」和两条「可能溢出」。点中任意一条,Source 窗口自动跳到对应位置,三个视觉标记配合使用:

代码行号左侧的红圆圈标出风险所在的行;被标红的代码是触发风险的具体表达式;表达式下方的小圆点标出风险产生的原因链条,鼠标悬停会弹出说明,告诉你「哪个变量的哪个取值导致了这次溢出」。第三层信息最容易被忽略,但它恰恰是最有用的——它把抽象的告警翻译成了具体的取值。

举个实际的例子:res = z + 5;这一行被标红,小圆点提示z可能取到 123,加 5 后得到 128,超出signed char的表示上限。看到这条说明,改法就明确了:要么把res提升为short或int,要么在赋值前对z加上限约束。相比之下,如果只看到「可能有溢出」这一句,很多人会去改z的定义,方向就跑偏了。

3.4 Result Details:把风险的传播路径摊开看

Result Details 窗口展示的是这次风险是怎么一步步走过来的:从函数入口开始,x被赋值为 1,y = x + 5推导出y恒等于 6,if( y < 0 )判定为假,该分支标记为不可达,z = extFunc( y )引入未知区间,两条分支各自推进,最终在加法处触发溢出判定。

这条链路的价值在于帮你区分「真问题」和「看起来像问题」。有些告警在 Result Details 里走两步就能看出是分析器对某个库函数的行为建模不准导致的,属于误报;有些告警的路径长得离谱,跨越四五个函数调用,那往往是真正值得警惕的深层耦合问题。我在实际项目里养成的习惯是:单函数内的告警快速扫一遍批量修,跨函数、跨文件的告警逐条看路径,因为后者通常牵扯接口契约,改一处会牵连上下游。

4. Code Prover 的四色语义:绿红橙灰分别对应什么代码事实

4.1 四色不是严重程度分级,是代码事实分类

很多人把 Code Prover 的颜色当成 Bug Finder 那种「严重程度」,这是理解偏差。四色描述的是分析器对某段代码在运行时行为的确信程度,是事实判断,不是风险排序。

颜色语义对应的代码事实处理方式
绿色绝对安全在分析器枚举的所有可能输入下都不出问题不用管,可作为验证通过的证据留存
红色确定存在错误至少存在一条具体路径必然触发运行时错误必改,且优先级最高
橙色可能出错是否出错取决于外部函数返回值、全局变量范围、未约束的输入补约束,再看转绿还是转红
灰色不会执行在当前约束下不可达的死代码判断是冗余还是遗漏,分别处理

绿色和红色都好办,灰色也相对简单,真正消耗时间的是橙色。它本质上是分析器在说「你给我信息不够,我没法下结论」。

4.2 橙色的本质:你欠分析器一个取值范围

回到样例代码。z = extFunc( y )这一行之后,分析器对z一无所知——extFunc只有声明,实现不在分析范围内,返回值的区间是完整的 -128 到 127。于是z + 5和z + 6两处加法都被判为橙色:如果z不超过 122,加法安全;如果超过,就溢出。分析器不会替你做假设,它只能报「可能」。

补约束的方式有三种,按侵入性从低到高排列:

第一种是在分析配置里直接限定变量的输入范围,图形界面在配置的输入范围设置项里逐个添加,命令行等价写法大致是:

# 限定外部函数返回值范围;参数名以 -help 输出为准,不同版本可能有差异 polyspace-code-prover \ -sources fun_test.c \ -lang c \ -compiler gcc \ -target x86_64 \ -results-dir ./ps_results_cp \ -variables-range "extFunc:return=-100..100"

-variables-range这类选项负责把「分析器不知道」变成「分析器按这个区间算」,写法是「变量或函数:属性=下界..上界」。区间给宽了,橙色不会消失;给窄了,等于人为屏蔽了真实存在的风险,比不设更危险。所以这个区间的来源必须是可信的,比如接口文档的约定、上游模块的实测数据,不能拍脑袋。

第二种是在代码里加断言。在调用extFunc之后插入一句范围检查,分析器会把这个断言当作后续路径的前置条件:

z = extFunc( y ); /* 断言把 z 的区间收窄到 [-100, 100],分析器据此重新推演后续路径 */ if (z < -100 || z > 100) { return 0; /* 契约被破坏时的兜底分支 */ }

这种写法侵入代码但换来两个好处:橙色判定变确定,而且断言本身成了运行时的契约检查,线上出问题时能第一时间拦下来。我在对可靠性要求高的模块里基本都这么干,代价是几行代码,收益是分析结论可复核。

第三种是把extFunc的实现也拉进分析范围,让分析器自己推。适用于外部函数实际是同工程内的模块,只是没导入而已。优点是不用人工给区间,缺点是分析规模上去了,耗时可能翻几倍。

约束补完之后必须重跑,看橙色变成了什么颜色。变绿说明这个区间内确实安全;变红说明区间内存在确定路径会炸,那就不是补约束能解决的,得改代码。

4.3 灰色死代码:删掉之前先问一句为什么

样例代码里的if( y < 0 )分支就是典型的灰色。y由x + 5推导,x恒为 1,y恒为 6,条件永远不成立,分支内y = 0这行代码永远不会执行。

灰色项的处理要分两种情况。如果这段代码是历史遗留的防御性判断,比如早期版本某个输入确实可能为负,后来逻辑改了但判断没删,那就清理掉,减少维护成本。如果是产品线共用的代码,在某些编译配置下这个分支是可达的,只是当前分析的配置没激活那条路径,那就不能删,得把相应的宏定义补进分析配置,让它重新变回可达状态。

我踩过的最深的一个坑是:某个函数里一大段代码全灰,查了半天以为是逻辑错误,最后发现是前面某个#ifdef的宏没定义,整段被预处理器剔掉了。所以看到大面积灰色,第一反应不该是「这些代码没用」,而是「我的配置是不是漏了什么」。

5. 避坑记录:五个让分析结果失真或白跑的操作

5.1 工程配置阶段的三处翻车

坑一:plain char 符号性与目标平台不一致

现象:本地跑出来一片绿,交付到目标板之后偶发负数处理错误,回头用目标平台的编译配置重跑,原来绿的地方全变红。

原因:char在 x86 上默认有符号,在部分嵌入式目标平台上默认无符号。分析配置沿用了桌面环境的默认值,所有涉及char的区间推导全部反了。

解决:新建工程的第一件事就是把目标平台的编译配置完整导入,尤其是符号性、整型宽度、对齐这几项。没有现成的配置文件就手工核对这些项,核对完在工程里存成模板,后续项目直接复用,别每次重来。

坑二:头文件路径少配一级

现象:Output Summary 刷出一片黄色告警,Dashboard 覆盖率只有百分之四十几,分析结果里出现大量「类型未知」相关的告警。

原因:Include Folders 只配到了顶层目录,嵌套子目录里的头文件找不到,相关类型全部退化成未知,分析器只能按最宽的范围去猜。

解决:把所有参与编译的 include 路径完整搬过来,包括第三方库的路径。配完先看 Output Summary 里黄色告警清零了没有,没清零就别往下看结果。这条我后来做成了强制检查项,每次跑分析前先扫一眼告警数。

坑三:宏定义没给全,代码进了分析器但走的是另一条路

现象:明明代码里有某个功能分支,分析结果里完全找不到它的踪迹,覆盖率也偏低。

原因:条件编译块由宏控制,分析配置里没定义对应的宏,预处理器直接把那段代码剔掉了。分析器看到的不是你实际编译的代码。

解决:把编译脚本里所有的-D定义原样搬到分析配置里。稳妥的做法是从构建系统导出编译命令,逐条对照,别靠记忆补。有条件的话把这份宏定义清单纳入版本管理,跟代码一起演进。

5.2 结果判读阶段的两处误判

坑四:把橙色当成「没问题」直接放过

现象:Code Prover 跑完,橙色项占了七八成,看一眼觉得「反正不是红色」,打包发版。

原因:橙色表示「取决于外部输入」,而现场环境的外部输入恰恰是最不可控的。橙色项在生产环境里变成实际故障的概率,远高于开发阶段的抽样测试能覆盖的范围。

解决:橙色项必须逐条给出去向——要么补约束后转绿,要么确认区间来源可信后加断言,要么改成红色后修掉。给不出结论的橙色项要登记在案,标注「当前无法约束,风险已知」,不能默默放过。

坑五:改了代码不重跑基线,拿旧结果对新代码

现象:代码改了三轮,报告里引用的还是第一轮的分析结果,评审时被问住。

原因:结果目录没有按版本隔离,新结果覆盖旧结果或者干脆没重跑,报告和代码对不上号。

解决:结果目录按提交号或日期命名,每次分析都生成新目录,报告里附上对应的代码版本号。这件事听起来琐碎,但真到了要回溯「这个风险是什么时候引入的」,没有版本对应的结果就只能重跑,时间成本翻倍。

6. 把误报压下去:约束补全与批量回归的两个手法

误报治理是长期活儿,靠人工一条条看撑不过三个迭代。我的做法是把重复劳动脚本化。

第一步,把分析命令固化成脚本,参数从配置文件读,结果按日期落盘:

#!/usr/bin/env bash # 每日回归:Bug Finder 快速扫描 + Code Prover 深度验证 set -euo pipefail STAMP=$(date +%Y%m%d_%H%M) SRC_LIST="./cfg/sources.txt" # 待分析文件清单,随代码演进维护 CFG="./cfg/target.conf" # 目标平台与宏定义,跟构建系统保持同步 polyspace-bug-finder -sources "$SRC_LIST" \ -options-file "$CFG" \ -results-dir "./out/bf_${STAMP}" polyspace-code-prover -sources "$SRC_LIST" \ -options-file "$CFG" \ -results-dir "./out/cp_${STAMP}"

-options-file是关键,把所有编译相关参数收进一个配置文件,脚本本身不写死任何平台细节,换目标平台只改配置。set -euo pipefail保证任何一步失败立即中断,避免拿半截结果当完整结果用。

第二步,对新旧两轮结果做差集,只关注新增项。老项目存量告警几百条,每次全看一遍不现实,增量对比才能让团队持续投入。这一步没有现成命令,常见做法是导出两轮的 Result List 到文本,按「文件:行号:规则类型」做键值比对,新增的键就是本轮引入的问题。

还有一个习惯值得坚持:每次补完约束、修完告警,把「改了什么、为什么这么改」写进提交说明,而不是只写「修复静态分析告警」。半年后回头看,你会庆幸当时多写了那两行——橙色转绿的依据、区间取值的来源,全靠这些记录撑着。从那以后我每次提交前都强制走一遍 Bug Finder,哪怕只改了一行,因为真正让分析工具产生价值的从来不是那一次全量扫描,而是把它变成日常动作里的固定环节。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询