前面三类堆栈统计工具,在统计堆栈使用的时候,其需要编译并运行代码,这意味着其需要定义测试激励,这种情况的堆栈统计是特定测试激励的,其他时候的堆栈大小则需要定义合适的测试激励。而要统计软件的堆栈需求,则需要设计合适的测试用例。这无疑对测试用例有比较高的要求。
形式化工具 Polyspace Code Prover 使用抽象解释法,能够深入探测到每一层函数的调用,统计每个函数本身的局部变量消耗和因为函数调用需要的栈消耗。另外,由于形式化的方法的使用,Polyspace Code Prover 能够分析代码中的分支是否因为上下文的原因不可达。这也会影响到实际程序中堆栈的大小。
Polyspace Code Prover能够统计以下信息:
栈使用量
栈使用量
程序栈使用量
程序栈使用量
局部变量使用量
局部变量使用量
其中局部变量使用量统计包括本函数的局部变量,函数的参数和返回值的开销,以及因为内存对齐导致的额外开销等,而栈使用量则包括局部变量使用量以及函数需要调用其他函数导致的开销。
程序的堆栈使用。当程序中有main函数或者其他的入口函数,Polyspace 可以统计主入口函数的总资源,其包括了调用其他函数需要的资源。
Polyspace 能够提供函数的调用关系图,据此可以看到一个函数的入口占用的资源是由于其调用了哪些函数带来的。
如上图我们知道入口函数 ps_main 调用了 SysTick_Handler 函数,也就是堆栈使用量 31 是 SysTick_Handler 调用引发的。
转到 SysTick_Handler 也能看到的确如此。
更复杂的在 scheduler_executive 调用,从下面调用图看到,其调用了多个函数,而虚的三角型则代表是通过函数指针这类方式进行非显式调用的。
那么如何知道各个调用的函数的资源开销呢,在上图点击转到定义,然后可以立刻查看该函数的堆栈使用
从上面几个图可以很明显的看到,update_shared_variables 占用了多的资源。随后我们可以继续往下跟踪。
何时执行堆栈分析?
执行堆栈分析是软件开发生命周期中的一个连续过程。如果仅在软件开发生命周期结束时由单独的质量评估团队估计堆栈使用量,则可能会使整个开发工作面临风险。此外,在开发周期的后期解决问题可能会出错且耗时;在确定是更改硬件还是软件设计时,这种做法可能还会造成混乱。
执行堆栈分析的时点是:
在添加新功能时
在软件中每添加一项新功能,都会使堆栈使用量增加。开发人员必须密切关注新功能的堆栈使用情况。
执行堆栈分析、进行调试和修复复杂代码:在每个主要功能实现后,开发人员可以在本地对特定软件组件或软件模块应用静态分析器,以评估基础软件和已实现软件之间堆栈使用量的增加情况。
在整个开发过程中监控堆栈分析:QA 团队和产品负责人可以使用静态分析器对持续集成 (CI) 管道进行堆栈估计,以在控制板上显示结果。此过程有助于在软件开发生命周期中跟踪堆栈分析。
执行良好实践以确保堆栈使用量:质量门有助于避免违反 MISRA? 和 AUTOSAR 编码规范。这些规范要求强制有条件地使用动态内存分配。
在软件发布前
静态分析器执行的堆栈估计提供了有力的证据,表明堆栈使用量处于控制之中。在每次软件发布之前,都应在标准工作负载、负载和负载下,对真实目标运行堆栈分析,以全面了解堆栈的使用情况。验证堆栈上溢和下溢事件的故障安全例程也至关重要。
根据堆栈统计进行资源使用优化
有了基础的统计信息,结合 Polyspace 提供的函数调用图,可以了解到在哪个位置,导致某个分支的堆栈使用量大增。
通过函数调用图,我们知道整个程序的情况:
我们可以定位相对消耗较大的模块,然后根据上述调用图,定位到其调用的模块,查看堆栈资源的消耗。
比如:
再到具体的函数中,查看其局部变量的消耗。
执行良好实践以确保堆栈使用量
对于产品级代码,推荐遵循行业的编码规范,如 MISRA C?、MISRA C++、AUTOSAR C++ 等。这些编码标准要求强制禁止动态内存分配,并推荐特定用例来优化静态内存分配。Polyspace Bug Finder? 有助于识别任何违反实践的行为,开发人员可以在本地监控这些行为,而产品负责人可通过 Polyspace Access 监控这些行为。以下编码规则详细说明了静态内存分配的实践。静态内存分配可以使用 Polyspace Bug Finder 进行分析。
以下是常见的编码规范中关于内存方面的部分。
编码规范
规则
描述
MISRA C:2004
20.4
不能使用动态堆内存分配。
MISRA C:2012
21.3
不能使用 <stdlib.h> 的内存分配和取消分配函数。
MISRA C++:2008
18-4-1
不能使用动态堆内存分配。
AUTOSAR C++14
A18-5-1
不能使用函数 malloc、calloc、realloc 和 free。
AUTOSAR C++14
A18-5-2
不能使用非定位 new 或 delete 表达式。
AUTOSAR C++14
A18-5-3
delete 表达式的形式应与用于分配内存的 new 表达式的形式保持一致。
AUTOSAR C++14
A18-5-4
如果为某个项目全局定义了运算符“delete”的有大小或无大小版本,则应同时定义有大小和无大小版本。
AUTOSAR C++14
A18-5-5
内存管理函数应确保以下各项:(a) 行为是确定的,能够预测出在差情形下的执行时间,(b) 避免内存碎片化,(c) 避免运行时出现内存不足,(d) 避免不匹配的分配或取消分配,以及 (e) 不依赖对内核的非确定性调用。
AUTOSAR C++14
A18-5-7
如果项目中使用动态内存管理函数的非实时实现,则只应在非实时程序阶段分配和取消分配内存。
AUTOSAR C++14
A18-5-8
存活期不超过函数的对象应具有自动存储期。
AUTOSAR C++14
A18-5-9
动态内存分配和取消分配函数的自定义实现应满足 C++ 标准中相应“必要行为”条款中指定的语义要求。
AUTOSAR C++14
A18-5-10
定位 new 运算符只能与对齐正确且指向足够存储容量的指针结合使用。
AUTOSAR C++14
A18-5-11
运算符“new”和运算符“delete”应一起定义。
Polyspace 提供常见内存问题的检查
??前一篇文章列举的一些缺陷,Polyspace Bug Finder 和 Polyspace Code Prover 提供了许多针对静态和动态内存分配的运行时检查。解决所有高、中和低优先级缺陷有助于降低内存分配带来的风险。
Polyspace Bug Finder 关于内存方面的检查项
此外,Polyspace Code Prover提供内存方面的形式化证明,包括
指针类型转换错误
数组越界
结构体指针越界
空指针或零地址解引用
对空指针偏移操作
位字段类型错误
malloc 返回值未检查是否为 NULL
联合体指针内存分配不足
结构体部分内存分配
结构体字段指针错误
函数返回局部变量指针
使用了已释放的内存无论使用何种方法来计算堆栈使用量,稍微增大堆栈大小都不失为一个好办法。这种方法有助于避免测试期间可能未检测到的堆栈溢出导致的系统漏洞。
堆栈溢出漏洞是许多嵌入式应用程序在实际运行中表现出不可定义行为的一个重要原因。在正确的时间使用正确的工具并遵循实践,可以增强对软件防止堆栈溢出的信心。