ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Polyspace静态分析入门:排查嵌入式C/C++溢出与死代码

Polyspace静态分析入门:排查嵌入式C/C++溢出与死代码

简介:《Polyspace工具使用说明》是一份面向嵌入式软件开发者、测试与代码审核人员的静态分析入门材料,围绕C/C++代码的运行时错误检测、编码规则一致性、静态度量与测试覆盖度展开,帮助读者理解如何借助Polyspace提升代码鲁棒性与安全性。资源包内共1个PDF文件,大小约828KB,篇幅集中,便于按主题查阅。内容覆盖Polyspace基本作用、新建工程与导入源文件、分析模块选择、配置项设置及检查运行流程,并分别说明Bug Finder的静态检测与Code Prover的深层验证。对Output Summary、Dashboard、Result List、Source和Result Details等结果窗口的解读也有交代,包括风险饼图、覆盖度柱状图、错误类型统计,以及绿、红、橙、灰四级代码安全标识和风险成因定位,并借示例源函数演示死代码、潜在溢出等问题的排查过程。目前已有2879人学习,适合需要快速建立Polyspace使用框架、对照示例代码理解静态分析结果的读者。

1. 什么是 Polyspace:先把它和“调试”分开

刚接触嵌入式软件的人常有个困惑:代码明明编译通过了,烧到板子上跑测试也没崩,为什么老工程师还要拿一个工具再把源码从头扫一遍?这有点像每年体检——你自己没觉得哪里疼,验血报告还是会告诉你几项指标偏高。Polyspace 做的就是给 C/C++ 代码做体检这件事:它不运行你的程序,而是把源码逐行读一遍,指出哪一行可能溢出、哪一段永远执行不到、哪条编码规则没守住。所以零基础入门 Polyspace,第一件要建立的认识是——它是静态分析工具,不是调试器,也不是仿真器。它适合做嵌入式软件、汽车电子、工业控制这类“改一版就要重新烧一次板子”的方向;如果你刚学 C 语言、想找个工具帮自己挑毛病,它同样能给你一份很具体的清单。

2. 为什么要学 Polyspace:编译器不报错,不代表代码没病

2.1 编译器只管语法,管不了你的值域

很多人第一次看到静态分析报告都会愣一下:这些代码我编译时一个警告都没有,怎么到你这里就一堆问题?原因是编译器和静态分析盯的根本不是同一件事。编译器关心的是语法对不对、类型搭不搭、函数有没有声明;它不会去算signed char加到 127 再加 5 会变成什么,也不会管你这个if分支是不是永远进不去。

举个最典型的例子:signed char在绝大多数嵌入式编译器上取值范围是 -128 到 127。你写res = z + 5,如果z是 127,结果就是 132,装不进 8 位有符号数里,实际落到变量里的值会绕回去变成负数。编译器不会拦你,因为语法完全合法;板子跑起来也未必立刻崩,可能只是在某个极端工况下算出一个离谱的值,然后你在现场花三天抓这个 bug。这种问题在 PC 上写业务代码的人可能一辈子碰不到几次,但在嵌入式里是家常便饭。

Polyspace 的价值就在这里:它在你提交代码之前,把这类“极端工况才炸”的路径先扫一遍。入门阶段不需要理解它内部用的抽象解释、值域分析这些词,你只要记住一句话——它替你把所有可能的输入都想过一遍,而不是像测试那样只跑了你写的那几组用例。

2.2 两个组件分工:Bug Finder 查缺陷,Code Prover 查运行时

Polyspace 里最容易被新手搞混的,是它有两个看起来很像的分析模块。名字都带 Polyspace,界面也长得差不多,但查的东西和给出的结论差别很大。入门时先把这张表记住,后面建工程选模块就不会选错。

对比项Polyspace Bug FinderPolyspace Code Prover
检查方式按缺陷模式和编码规则逐条匹配对变量的取值范围做推演,验证运行时行为
是否需要运行代码不需要不需要
典型输出缺陷列表、规则违背项、代码度量按颜色标记每一行代码的安全等级
结论口吻“这里可能有问题”“这里一定安全 / 一定出错 / 取决于外部输入”
适合什么时候用日常提交前扫一遍,卡编码规范关键模块交付前做深度验证

简单说,Bug Finder 更像一个经验丰富的审查员,拿着检查清单一条条对:你有没有用未初始化的变量、有没有数组越界、MISRA 规则里哪条没遵守、这个文件有多少行代码、圈复杂度多少。它输出的是“问题条目”,一条一条列给你。

Code Prover 则更像做数学证明:它把每个变量的可能取值范围推出来,然后判断某一行代码在这个范围下会不会出事。所以它的结果不是条目,而是颜色——绿色表示在所有可能输入下都安全,红色表示确定会出事,橙色表示取决于外部条件,灰色表示这行根本执行不到。这两种视角互补,入门阶段建议先用 Bug Finder 建立感觉,等你能看懂报告结构了,再上 Code Prover 做深一层验证。

提示:两个模块的分析流程几乎一样,都是建工程、导源码、选模块、配参数、跑分析。所以你只需要学会一遍流程,切换模块就是换个选项的事。

2.3 一段 19 行的小代码,藏着四处隐患

光讲概念容易飘,直接看一段代码。这段代码短到你可以背下来,但它同时包含了溢出、死代码、外部依赖三类问题,是新手练习最合适的样本。

/* fun_test.c:一段不到 20 行的示例函数,故意留了几处隐患 */ extern signed char extFunc(signed char y); /* 外部函数,只有声明,没有实现 */ signed char firstfun(void) { signed char x = 1; /* 初值确定,x 恒为 1 */ signed char y; signed char z; signed char res; y = x + 5; /* 因为 x 恒为 1,所以 y 恒为 6 */ if (y < 0) { /* 6 < 0 永远不成立,这个分支进不去 */ y = 0; } z = extFunc(y); /* z 的值完全由外部函数决定,工具看不到它内部 */ if (z <= 123) { res = z + 5; /* z 若取 127,127 + 5 = 132,超出 signed char 上限 */ } else { res = z + 6; /* 另一条分支同样存在加法越界 */ res = res / 2; } return res; }

逐点解释一下这段代码为什么会被工具盯上。第一处,x被赋值为 1 之后没有再改过,所以y = x + 5的结果恒为 6,if (y < 0)这个判断永远为假,花括号里的y = 0;是一次都执行不到的死代码。第二处,z来自extFunc,这个函数只有声明没有实现,工具无法知道它返回什么,只能按signed char的完整范围 -128 到 127 去估算。第三处,res = z + 5在z取 123 到 127 时会超过 127 的上限,发生有符号溢出。第四处,else分支里的res = z + 6有同样的问题,只是方向不同。

这里有个参数值得单独说:signed char的取值范围取决于编译目标的设置。有些嵌入式编译器默认char是无符号的,这时候变量范围会变成 0 到 255,溢出位置也跟着变。这不是玄学,是编译选项决定的,你在配置分析时会遇到这个开关,到第 3 章再具体说。

2.4 新手先盯哪几类告警

第一次跑完分析,报告里可能出现几十上百条结果。新手最容易犯的错是从第一条开始逐条抠,抠到第十条就烦了,然后关掉工具再也不打开。正确的顺序是按危害排优先级,先看会真正让程序算错的,再看影响可维护性的。

我的建议是这样一个顺序:先看数组越界、指针越界、除零、有符号溢出这几类,它们会直接算出错误结果;再看未初始化变量和返回值未检查,这类问题在嵌入式里经常表现为偶发死机;然后看死代码,它往往说明你的逻辑写错了或者条件写反了;最后才看编码规则一致性,比如 MISRA 那一类,它们更多是团队协作和可维护性的问题,不会立刻让程序崩。

至于静态度量,比如代码行数、函数调用次数、圈复杂度,那是给项目管理和代码评审用的参考数据,入门阶段扫一眼知道有这回事就行,不用纠结数值高低。后面第 4 章讲怎么看懂这些结果窗口,第 5 章讲新手最容易在哪儿翻车。

3. 第一次跑通:从新建工程到看见第一条告警

3.1 准备一个能被分析的最小工程

不要一上手就拿公司项目的主控代码去分析,几千个文件、几十个头文件路径,光是编译配置就能卡你半天。第一次跑,目标是“看见结果”,不是“查出所有问题”。所以我建议你自己搭一个只有三个目录的小工程,把第 2 章那段fun_test.c放进去。

demo_project/ ├── src/ │ └── fun_test.c # 待分析的源文件,只有这一个 ├── include/ # 头文件目录,本例为空,但目录要建着 └── polyspace/ # 分析工程文件放在这里,和源码分开

为什么要把分析工程单独放一个目录?因为 Polyspace 会在工程目录下生成一堆中间文件和结果文件,如果和工作源码混在一起,以后你提交代码时很容易把这些几百兆的结果文件一起提交上去。这个习惯我从第一次用就养成了,后面换项目也没改过。

源文件就照抄第 2 章那段,一个字都不用改。它足够短,短到你跑完之后可以自己推一遍工具给的结论对不对,这对建立信任感很重要——你要能验证工具说的对,才会真的用它。

3.2 五步建工程,每一步在填什么

打开 Polyspace 之后,新建工程的入口在菜单里,点进去会看到一个向导。向导里有五个地方需要你填,下面按顺序说清楚每一处是干什么的,以及第一次该怎么填。

第一步,新建工程。给它起个名字,比如demo_first,位置选到刚才那个polyspace/目录。这一步只是建一个容器,还没有任何代码进去。

第二步,导入分析代码。这里有两个输入框最容易被新手忽略。一个是 Project Source Files,填你要分析的源文件,本例就是fun_test.c的完整路径;另一个是 Project Include Folders,填头文件搜索路径。本例的fun_test.c只声明了一个外部函数,没有#include任何头文件,所以这一栏可以空着,但目录建议先建好,因为一旦你的代码里出现#include "xxx.h",工具找不到这个头文件就会在分析开始时直接报错停下。

第三步,选择分析模块。向导会让你选这次跑哪个模块,Bug Finder 还是 Code Prover。第一次建议选 Bug Finder,原因很实际:它出结果快,报告结构是条目式的,比一堆颜色更好理解。选完之后,向导里对应会出现 Module Source Files、Configuration、Result 三个区域,分别对应“分析哪些文件”“用什么配置”“结果存哪里”。

第四步,配置分析项。Configuration 这一栏里有一堆选项,第一次全部保持默认,一个都别改。默认配置已经能查出溢出和死代码,够你跑通第一个例子了。等你能看懂报告了,再回头调这些参数。

第五步,运行检查。点开始,工具会先编译一遍你的源码,然后开始分析。这里有个小选项是“是否生成新的结果文件”,第一次勾上,方便你跑完之后反复打开看,不用重跑。

注意:分析前的编译和你平时用编译器编译不是一回事。工具内部会用自己的前端把源码解析一遍,如果这一步失败,后面的分析根本不会开始。所以看到红色的编译错误,先解决它,别急着怀疑分析功能。

3.3 配置项别乱动,先只关心这三处

虽然建议第一次全用默认,但有三处配置你迟早要碰,早点知道它们在哪、影响什么,后面少走弯路。

第一处是目标编译器相关的设置,特别是char是否有符号。前面说过,signed char和unsigned char的取值范围差一倍,直接影响溢出判断的结论。如果你的代码要跑在某个特定的嵌入式编译器上,这里要选成和它一致的设置,否则工具算出来的溢出位置和你实际运行时的位置对不上,你会以为是误报。

第二处是外部函数的处理方式。像extFunc这种只有声明没有实现的函数,工具默认按返回类型的完整范围去估。你可以给它写一个简单的桩函数,或者直接用约束语句告诉工具“这个函数返回值在 0 到 100 之间”,橙色告警就会大幅减少。这个技巧在真实项目里非常有用,因为嵌入式代码大量依赖硬件寄存器读取函数,那些函数的返回值范围你其实是知道的。

第三处是检查规则集。默认配置下 Bug Finder 会开一批通用缺陷检查,MISRA 那类编码规则通常是可选项。第一次跑不要全开,全开的结果是报告里几百条规则违背,把真正危险的内存问题淹没了。

3.4 点下运行之后,第一眼看 Output Summary

分析跑起来之后,界面上会有一个输出摘要窗口,实时刷进度和日志。这个窗口新手往往一扫而过,其实它是最该先看的地方。

如果分析正常结束,这里会告诉你一共分析了几个文件、耗时多少、生成了什么结果。如果中途出问题,这里会冒出红色或黄色的提示。红色通常意味着编译阶段就失败了,比如文件路径写错、头文件找不到、语法有 tool 不认识的地方;黄色一般是警告,比如某个文件被跳过了、某个配置项取值不太对但还能继续跑。

我第一次跑的时候就栽在这一步:源文件路径里手写了一个中文目录名,工具没找到文件,输出窗口刷了一行黄色警告就继续跑下去了,最后结果是空的。我盯着空报告看了十分钟,以为是工具没装好。所以养成习惯——跑完先扫一眼这个窗口有没有红黄提示,再去读结果。

4. 看懂结果:五个窗口按什么顺序读

4.1 Output Summary 和 Dashboard:先看颜色比例

分析结束之后,结果会分成几个窗口展示。第一个要看的是 Dashboard,它把整体情况浓缩成几张图。

最上面通常是风险数量的饼图,红色代表高风险,粉色代表低风险。你不需要记住每一条是什么,先看比例:如果红色占了一大半,说明这份代码的问题密度很高,可能连基本的初始化都没做好;如果红色只有一两块,那就可以逐条看了。旁边一般是分析覆盖度的柱状图,它告诉你这次分析覆盖了多少函数、多少分支,覆盖度低往往意味着有大量代码因为编译问题被跳过了,这时候报告再漂亮也不可信。

再旁边是错误类型和数量的分布图。这张图对新手特别友好,它能一眼告诉你这份代码的主要问题集中在哪:是溢出多,还是未初始化多,还是规则违背多。你按类型去处理,比按文件顺序一条条抠效率高得多。

4.2 Result List 和 Source:从告警跳到那一行

Dashboard 上的每一块颜色都可以点,点完之后 Result List 窗口会列出对应的具体条目。每条记录一般包含几个信息:问题类型、所在文件、行号、数量。

拿第 2 章那段代码举例,Result List 里大概率会出现这么几条:一条是“死代码”,位置在if (y < 0)里面的y = 0;那一行;另外两条是“可能溢出”,位置分别在res = z + 5和res = z + 6。这和我们自己推的结论是一致的,正好可以拿来验证工具的判断。

点中某一条,Source 窗口会自动跳到对应的源码位置。这个窗口里有几个视觉标记值得记住:行号左边会出现一个小圆点或圆圈,标出问题所在行;有问题的代码行会被染色;如果问题是由前面几行推导出来的,那些行下面会有小点标记,鼠标移上去会弹出一段说明,告诉你“因为 x 在这里被赋值为 1,所以 y 恒为 6”。

这个交互设计其实是整个工具里最有价值的部分。新手看报告最怕的就是“它说这里有问题,但我看不出为什么”,而那些小点就是把推理链条摊开给你看。我第一次用的时候就是靠这个功能,才真正明白“值域推演”这四个字在做什么。

4.3 Result Details:把“为什么报”翻译成人话

如果你点开某一条结果的详情,会看到一个更详细的说明窗口。它一般会讲三件事:这个问题属于哪一类、触发它的条件是什么、可能导致什么后果。

比如溢出那条,详情里会写清楚:变量z的取值范围是 -128 到 127,z + 5的结果范围是 -123 到 132,而目标类型signed char的上限是 127,因此在z大于等于 123 时结果会被截断。这段话读起来有点绕,但它其实就是在替你把数学推一遍。

新手常见的误区是只看结论不看详情,然后凭感觉改代码。比如看到溢出就把res改成int,改完之后溢出没了,但你可能因此改变了函数的返回类型,调用方全部要跟着改,问题从“算错”变成“编译不过”。先读详情,搞清楚触发条件,再决定是改类型、加范围约束,还是在上游就把z限死。

4.4 Code Prover 的四种颜色,对应四种处理方式

如果你切到 Code Prover 跑同一段代码,结果呈现方式会完全不同:Source 窗口里每一行代码都被染上了颜色。四种颜色的含义一定要记牢,它们直接对应四种处理动作。

绿色表示这行代码在所有可能的输入下都安全,你不用管它。红色表示确定会出问题,这种必须改,没有商量余地。橙色表示取决于外部条件,比如某个外部函数的返回值、某个全局变量的范围,这类要逐个确认——如果你能证明实际运行时不会取到危险值,可以通过加约束让它变绿;如果证明不了,就得加保护代码。灰色表示死代码,永远执行不到,要么是逻辑写错了,要么是可以删掉的冗余。

这四种颜色里,新手最容易误解的是橙色。看到一片橙色会觉得“这工具是不是什么都报”,其实橙色恰恰是最有价值的一类——它在告诉你“这里的正确性依赖一个你自己也没把握的假设”。把橙色一条条确认掉的过程,就是把你脑子里那些模糊的假设变成明确约束的过程。

5. 避坑与常见问题排查:新手最容易翻车的五个地方

5.1 一上来满屏红色编译错误

现象:点下运行之后,Output Summary 里刷出一堆红色提示,大意是找不到某个文件或者某个符号未定义,分析直接中断,一行结果都没有。

原因:绝大多数情况是头文件路径没配对。你的源码里写了#include "xxx.h",但这个头文件所在的目录没有加到 Include Folders 里,工具解析到这一行就断了。另一种情况是源文件列表里漏了某个.c文件,导致里面定义的函数在别处被调用时找不到实现。

解决:先看红色提示里点名的是哪个文件、哪一行,顺着这个线索找它需要的头文件在哪个目录,把那整个目录加进 Include Folders。如果是符号未定义,检查 Source Files 列表是否完整。这个排查过程看起来笨,但比重装工具快得多。

5.2 结果全是橙色,看着像全错

现象:Code Prover 跑完,满屏橙色,几乎没有绿色,你开始怀疑自己写的代码是不是一无是处。

原因:橙色不是“错”,是“不确定”。最常见的不确定来源是外部函数——硬件抽象层的读寄存器函数、通信收到的数据、从 flash 里读出来的配置,这些值工具都看不到范围,只能按类型全范围估。一旦某个变量范围被放大,它参与的每一次运算都会传染成橙色。

解决:给关键的外部函数加返回值约束,或者写一个简单的桩函数返回一个有界值。约束写完重跑,你会发现一大片橙色变绿。这个过程其实是在逼你把“我假设这个寄存器读出来在 0 到 255 之间”这种口头假设写成代码,长期看是好事。

5.3 灰色死代码被当成误报

现象:报告里标了一片灰色,说这段代码永远执行不到,但你确信业务逻辑上它是有用的分支。

原因:死代码判定是基于值域推演得出的,如果工具认为条件恒真或恒假,就会把对应分支标灰。常见触发场景是:某个标志位在你分析的这份代码里从来没被赋过其他值,或者循环条件里的变量范围被推成了空集。还有一种情况是这段代码只在中断里被修改,而工具的分析是单线程视角。

解决:先顺着灰色标记往上找,看是哪个变量的范围导致条件恒成立。如果确认是中断或并发场景下才会改变,那属于分析配置的问题,需要在配置里说明这个变量的变化来源,或者在分析时把它设为 volatile 并给出范围。别直接忽略灰色,它十有八九在提示你某处逻辑和你以为的不一样。

5.4 同一份代码两次跑结果不一样

现象:代码没改,换了台机器或者隔天再跑,告警数量变了,多了几条也少了几乎。

原因:多半是配置没有跟着工程走。Polyspace 的工程文件会记录一部分配置,但编译目标设置、规则集开关这类选项,如果是在界面临时改的而没有保存进工程,换台机器就回到默认值了。另一种可能是两次分析用的源码版本不同,你以为没改,其实同事提交了一版。

解决:把关键配置固定下来,写进工程文件,团队里统一用同一份工程配置。跑分析前确认代码版本,分析后把结果文件和配置一起归档。这个习惯听起来麻烦,但出问题回溯时能救命。

5.5 第一次就开全规则,报告几千条

现象:想着“既然要用就用到极致”,把能开的检查规则全勾上,跑完出来几千条结果,看一眼就关掉了。

原因:编码规则类检查和缺陷类检查混在一起,前者数量级远大于后者,而且很多规则违背并不影响功能,只是风格问题。报告被淹没之后,真正危险的内存越界和溢出反而找不到了。

解决:分两轮跑。第一轮只开缺陷类检查,把会算错结果的问题清干净;第二轮再开规则集,而且按模块分批处理,一次只看一个文件的规则问题。这个过程我踩过坑,第一次全开的结果就是放弃使用工具三个月。

6. 下一步学什么:从会跑例子到敢在项目里用

跑通了第一个例子、能看懂红橙绿灰,这已经足够让你在团队里接手静态分析这部分工作了。接下来怎么往上走,我按三个台阶说。

阶段目标具体做什么
第一台阶把流程跑顺换三份不同的源码各跑一遍,包括一份带多目录头文件的项目,练到不用看文档就能建工程
第二台阶会收敛结果学会给外部函数加约束、写桩函数,把橙色压下去,让报告里的每一条你都能解释
第三台阶接进流程把分析配置固定成模板,每次提交前跑一次 Bug Finder,只关注新增的红色条目

第一台阶的关键是换环境。你现在只在一个单文件工程上跑过,真实项目的目录结构、头文件依赖、第三方库会复杂得多,早点碰这些麻烦比晚点碰好。建议找一个有两三层目录、带几个#include的小项目练手,专门练头文件路径配置这一项。

第二台阶是分水岭。能不能把橙色收敛掉,决定了你是在“用工具”还是“被工具用”。做法就是给每一个不确定的来源找答案:这个寄存器读出来到底几位有效?这个通信帧的长度字段有没有上限?找到答案就写成约束,写不了就加保护代码。这个过程走完一遍,你会发现自己对代码的理解比写的时候清楚多了。

第三台阶是把它变成习惯。一个人的时候可以靠自觉,进了团队就得靠流程。把配置固化成模板,把分析卡在提交之前,只比对新增的红色条目,老的存量问题单独排期处理。

最后说个我自己的教训。刚学的时候我为了图快,看报告只扫一眼红色条目,橙色和灰色一概跳过,觉得那是工具在瞎操心。结果有个模块上线后偶发死机,回头一查,正是当初被我跳过的一条橙色——某个外部函数的返回值我默认它不会超过 100,实际现场有一批硬件返回了 200 多。从那以后我每次跑完分析,橙色条目一律逐条过一遍,哪怕当场判不了也要写个备注,绝不留给“以后再说”。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进