尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Polyspace静态分析配置实战:嵌入式代码安全与功能安全

Polyspace静态分析配置实战:嵌入式代码安全与功能安全 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 路径。第三坑配置语言标准。编译器选项里的-stdc99还是-stdc11会直接影响 Polyspace 对代码语义的理解。比如_Bool在 C99 里是内置类型在 C89 里就不是。你代码里什么都没变语言标准配置错了Pre-Processing 阶段就会报错。我的建议是在创建项目阶段多花十分钟把编译器类型和语言标准核对准确这十分钟后面能帮你省十个小时的排查时间。反正我踩过的那些“莫名字法错误”回头看八成都是编译器配错了。3.3 头文件路径与宏定义配置编译器解析完毕之后Polyspace 会在项目设置里自动填入一大堆 Include Path 和 Define。这个时候不要全信一定要在 Analyze 之前检查一遍也可以配置一个 Initial Analysis Setup 步骤自动检查。怎么检查在项目界面的 Project Browser 里展开项目树找到 Compiler Settings 相关节点翻看头文件路径列表。确认以下两件事所有项目内部的相对路径都正确解析。有实际条件编译的宏比如STM32F407xx、PRODUCTION_BUILD在 Define 列表里。宏定义这块我必须多说一句。真实工程里条件编译几乎无处不在。比如#ifdef TEST_MODE buffer_size 128; #else buffer_size 256; #endif如果不加TEST_MODE宏Polyspace 默认走#else分支分析的就是生产代码。如果你正好想测测试模式的代码那就得在配置里加上这个宏。反过来说如果生产代码和测试代码在逻辑上差别很大而你两种都想覆盖就得建两个不同的项目配置分别定义不同的宏组合。在我实际用过的项目里最复杂的宏组合来自芯片厂商的 SDK。一个 Sensor 库头文件里可能嵌套了几百个宏分支不同的片子配置不同的宏。没有在项目配置阶段把宏理清楚后面根本没法看结果。这份功夫不能省。4. 核心配置详解把“运行环境”翻译给 Polyspace4.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 提供了一系列代码注释指令比如/* polyspaceCALL */ /* polyspaceDEFINE */ #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 会有一个运行监视面板我一般盯这么四个指标编译阶段是否通过是不是有文件解析失败了。是否出现了PolyspaceUNKNOWN类异常这通常意味着某个语句的语义不能被工具理解可能是编译器特性或汇编代码导致。分析进度百分比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 resultsCode 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-analyze7.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 Stage或Compilation Stage的报错。编译阶段没过之后分析阶段的行为都不可信。先解决编译阶段的报错再谈后面的分析结果。第三步简化复现。如果你怀疑某个特定函数或文件导致异常临时建一个只包含该文件的配置跑通后再逐步加回其他文件。二分定位法在这种场景下很高效。这套方法论我已经在多个项目里验证过每次都能把排查时间缩短到半小时以内。建议收藏备用。8. 最后再分享一点个人经验如果你是从零开始接触 Polyspace我的建议是先别急着追求“零告警”。Polyspace 更像一面放大镜它会把代码里所有未定义的行为和潜在风险放大给你看。这种可能性的暴露在一开始往往让人难以接受。但这个过程是必经的。你花在配置上的每一分钟都会在后面减少十分钟的无效告警排查。还有一点不要迷信工具也不要全盘否定工具。Polyspace 有它很强的能力也有它理解不了的场景比如高耦合的多任务并发访问、复杂的动态调度策略。在这些场景下它给出的结果只是“参考意见”最终决策还是得靠人来判断。工具是把你的经验和判断力放大了而不是替代了它。我这些年配置 Polyspace 关注的最重要的一件事配置不是技术问题而是“怎么把自己的领域知识结构化地表达给工具”的问题。一旦你建立起这个认知遇到再复杂的配置需求你都能找到答案。希望这些内容对你有用。如果你在配置过程中碰到什么绕不过去的怪问题欢迎在评论区留言我会尽量回复。
返回列表