堆栈统计知多少?(内存泄漏、栈溢出、堆栈溢出,堆栈统计,堆栈常见bug)

摘要

本文主要聊聊关于堆栈的内容。包括堆栈和内存的基本知识。常见和堆栈相关的bug,如栈溢出,内存泄漏,堆内存分配失败等。后面介绍软件中堆栈统计的重要性,以及如何使用工具工具软件中堆栈使用的范围,并给出在软件开发中,如何降低堆栈问题,优化堆栈的一些实践。
本文旨在抛砖引玉,欢迎留言讨论。

堆栈和内存

内存是计算机中重要的单元,而堆栈是内存中最重要的应用组成。

C语言的内存分配

在嵌入式系统中,内存通常被分为几个主要区域,每个区域存储不同类型的数据,这些区域在使用方式、性能以及目的上各不相同。关于这些区域,可以参考。

C语言的内存分配{静态内存&动态内存&堆栈}

下面主要说说堆区和栈区

堆区

堆区用于动态内存分配,程序运行时可以从堆区动态地分配和释放内存。其管理通常由程序的内存管理子系统(如C语言的malloc和free函数)负责。堆的大小和使用效率直接影响程序的性能和稳定性。堆区同样位于RAM中,因此其内容在断电后会丢失。

栈区

栈区主要用于存储局部变量、函数参数和返回地址等。栈具有后进先出(LIFO)的特性,每当调用新的函数时,函数的局部变量和返回地址就会被压入栈中,函数返回时这些数据又会被弹出。栈的使用高效但空间有限,栈溢出是嵌入式系统中常见的问题之一。

堆区(Heap Memory)和栈区(Stack Memory)有以下几个主要的区别:

  • 管理方式:
    • 栈是自动管理的,由编译器控制。当函数调用时,栈帧(Stack Frame)被自动创建和销毁。
    • 堆是手动管理的,需要程序员使用特定的函数(如mallocfree在C中)来分配和释放内存。
  • 分配和释放速度:
    • 栈的分配和释放速度通常较快,因为它们是连续分配的,并且不需要复杂的内存管理。
    • 堆的分配和释放速度较慢。差距的时间主要用在操作系统查找空闲内存块,并可能涉及将内存块从非连续的区域移动到连续的区域。堆上分配内存不连续也导致内存的碎片化。
  • 内存碎片:
    • 栈内存是连续分配的,通常不会产生内存碎片。
    • 堆内存是动态分配的,可能会产生内存碎片,这需要定期进行内存整理(Memory Compaction)来优化性能。

堆栈统计为什么如此重要?

从上面关于堆栈的常识,我们知道堆栈的大小是有限的,堆栈对于程序运行极为重要。

在设计嵌入式系统时,合理规划和管理内存区域对于确保系统的性能、稳定性和可靠性十分重要。开发者需要根据应用的具体需求和硬件资源,做出恰当的内存使用决策。

在嵌入式系统中,堆栈用于存储函数调用时的返回地址、局部变量、寄存器状态等信息。堆栈的大小直接影响到程序的运行稳定性和性能。对于安全关键嵌入式系统而言,堆栈不足可能导致系统崩溃、数据损坏甚至安全事故。因此,合理分配堆栈大小,通过堆栈统计来优化堆栈使用,对于确保系统的可靠性和安全性至关重要。

内存管理不当,会导致内存泄露(堆泄露)。而内存泄漏可能会堆栈的不足,进而出现堆栈溢出,这些是编程中常见的错误之一,而且及其严重。对于常规的桌面级应用,这些错误发生会导致程序卡顿,甚至重启崩溃。而对于涉及生命安全和重大财产安全的关键应用系统和软件,其系统不稳定和崩溃会引发严重后果,包括产品召回、系统失效甚至人员伤亡和财产损失。

内存管理不当

下面通过一些举例说明典型的问题。
Buffer overflow(缓冲区溢出)是一种常见的安全漏洞,通常发生在当程序试图向一个固定长度的缓冲区写入过多数据时。尽管缓冲区溢出通常与堆栈溢出有所区别——前者涉及对固定大小缓冲区的写操作超出其边界,后者是函数调用和局部变量使用过多堆栈空间——但在实践中,缓冲区溢出经常导致堆栈上的数据被覆盖,因此可以视为一种堆栈不足引发的问题。

以下是一个使用C语言的示例,展示了一个简单的缓冲区溢出漏洞:

#include <stdio.h>
#include <string.h>
void vulnerableFunction(char *str)
{
	char buffer[10];	
	strcpy(buffer, str); // 不安全的拷贝,拷贝应该指定大小	
	printf("Buffer content: %s\n", buffer);
}

int main() {
	char largeData[] = "这是一个超长的字符串,远远超过了buffer的容量";
	vulnerableFunction(largeData);
	return 0;
}

在这个例子中,vulnerableFunction 函数定义了一个长度为10的字符数组buffer作为缓冲区。然后,它使用strcpy函数将传入的字符串str拷贝到buffer中。如果str的长度超过了buffer的容量(在这个例子中是10个字符),就会发生缓冲区溢出。strcpy不会检查目标缓冲区的大小,所以它会继续写入数据,直到遇到源字符串的结束符\0

这种溢出可能会覆盖堆栈上的其他重要数据,比如其他局部变量、函数返回地址等,导致程序行为异常,甚至允许攻击者执行任意代码。

为了避免这种安全漏洞,应该使用更安全的函数,如strncpy,它允许指定目标缓冲区的最大长度,从而避免溢出:

strncpy(buffer, str, sizeof(buffer) - 1);

buffer[sizeof(buffer) - 1] = ‘\0’; // 确保字符串以null结束

这样就可以显著减少因缓冲区溢出导致的安全风险。

其他一些关于堆栈溢出的例子。

Memory Allocation Failed

当请求的内存无法被分配时发生。当请求大量内存时,可能会因为内存不足或者没有足够的连续内存导致分配内存失败。

#include <stdlib.h>
int main() {
	//分配大量内存
	int *bigArray = (int*)malloc(sizeof(int) * 1000000000);
	if (bigArray == NULL) {
	printf("Memory allocation failed\n") //内存分配失败
}
	free(bigArray);
	return 0;
}

Memory Leak

内存泄漏更准确的说法是,堆内存泄漏(heap leak),是程序员在分配一段内存后,分配的内存未被释放且无法再次访问时发生。

#include <stdlib.h>
void leakMemory() {
	int *leak = (int*)malloc(sizeof(int) * 100);
	// 漏掉了释放操作
}

int main() {
	leakMemory();
	return 0;
}

例子中,指针leak作为局部变量,在退出leakMemory函数后,没有释放且找不到地址无法再次访问。

Stack Leak

当程序中的局部变量大量消耗栈资源,而又没有退出该函数,导致stack溢出,大量的溢出可能会导致栈的不足,从而发生overflow的情况。这种一般发生在递归函数或者函数中有大循环,其有定义局部变量,比如下面的代码

void stackLeak(int n) {
	char buffer[1024];
	printf("Leaking stack memory %d,%p\n", n, (void *)buffer);
	if(n>1)
	stackLeak(n - 1);
}
int main() {
	stackLeak(500);
	return 0;
}

在32G内存的笔记本上,运行到373次就栈溢出了。

在这里插入图片描述

Stack Frame Corruption

栈帧中是函数的局部变量和函数调用时候的相关开销。当我们对局部变量进行错误的操作时候,可能会破坏栈帧,导致函数的返回地址或其他重要数据被覆盖。举例如下。

#include <stdio.h>
void corruptStackFrame() {
	int arr[1] = {0};
	int b = 10;
	int c = 20;
	arr[1] = 0; // 故意写入数组界限之外,可能覆盖返回地址
	arr[2] = 0; // 故意写入数组界限之外,可能覆盖返回地址
	printf("b=%d c=%d\n", b, c);
}

int main() {
	corruptStackFrame();
	return 0;
}

上面的例子中,有明显的数组越界的问题。同时,由于该数组是局部变量,对数组外的数进行操作,可能会导致周边的栈帧给改写,从而导致系统崩溃。

在这里插入图片描述

当然栈帧是否被改写可能涉及很多系统的很多方面。这里不详细讨论。

历史上,许多著名的软件漏洞,如 Heartbleed、Spectre 等,都与堆栈管理不当有关。通过对堆栈进行统计,我们可以提前发现潜在的安全隐患,避免类似问题的发生。

Heartbleed 漏洞是由于 OpenSSL 库中的堆栈管理错误导致的。该漏洞允许攻击者读取内存中的敏感信息,甚至可以修改内存内容。通过对堆栈进行统计和分析,可以发现 OpenSSL 库中的堆栈使用不当,从而避免 Heartbleed 漏洞的产生。

关于heartbleed,可以参考链接了解更多

https://heartbleed.com/

堆栈统计促进软件开发和性能优化

堆栈统计不仅可以帮助开发者确定程序在运行时堆栈的使用情况,还可以指导开发者进行性能优化。通过准确的堆栈使用数据,开发者可以合理分配堆栈大小,既避免了堆栈溢出的风险,也确保了系统资源的高效利用。

  1. 性能瓶颈定位:堆栈统计可以帮助开发者快速定位应用程序中的性能瓶颈。通过分析哪些函数调用最频繁或哪些调用耗时最长,开发者可以集中优化这些热点区域,从而提高整体应用性能。
  2. 资源使用分析:它可以帮助开发者理解应用程序如何使用系统资源,例如CPU和内存。这对于识别和修复内存泄漏、过度的CPU使用等问题非常重要。
  3. 代码质量改进:通过堆栈统计,开发者可以识别代码中的不良实践或设计模式,如不必要的递归、过度复杂的函数调用等,进而重构代码以提高其可读性和可维护性。
  4. 优化决策依据:堆栈统计提供了量化数据,帮助开发团队做出基于数据的决策。这些数据可以用来确定优化的优先级,决定哪些优化措施可以带来最大的性能提升。
  5. 性能回归检测:在软件开发周期中,新的代码提交可能会引入性能回归。定期进行堆栈统计可以帮助及时发现性能下降,确保软件性能持续稳定。
  6. 用户体验提升:应用程序的响应速度直接影响用户体验。通过优化那些影响性能的关键部分,可以显著提升应用的响应速度和流畅度,从而提高用户满意度。
  7. 成本效益分析:对于需要大量计算资源的应用程序,堆栈统计可以帮助识别和优化资源密集型操作,从而减少对硬件资源的需求,降低运营成本。
    总之,堆栈统计是理解和优化软件性能的强大工具。通过定期进行堆栈统计和分析,开发团队可以确保他们的应用程序运行高效,提供优秀的用户体验,并以最佳的资源使用效率运作。

人工VS工具统计

在有相关统计工具之前,嵌入式系统的堆栈都是人工统计。此举虽然可行,但是实际操作中存在许多问题和不足。

耗时耗力烧脑,对于复杂的嵌入式系统而言几乎是不可行的。
没法保证准确性,特别是在面对大量并发执行的任务和复杂的函数调用关系时,更是这样。
实时更新,则更是无法做到,每一次代码的变更,可能都需要做大量的重新统计。
工具统计除了解决上面人工统计的问题之外,还有另外两个优势。

动态分析:一些高级的堆栈统计工具支持运行时分析,能够实时监控堆栈的使用情况,及时发现潜在的堆栈溢出风险。这种一般是芯片厂家或者合作厂家提供的调试工具中。
可视化效果好。许多工具提供图形化界面,直观展示堆栈的使用情况,函数调用情况以及随着函数调用,堆栈使用的变化,帮助开发者更容易理解和分析数据。

市面上常见的堆栈统计软件

堆栈统计工具有以下几类
一类是由编译和 调试工具,有芯片厂商提供的,也有第三方的。比如ARM Keil MDK,IAR Embedded Workbench等等
第二类是第三方的调试工具,GNU项目,如GNU Debugger,Lauterbach Trace32
第三类是开源堆栈工具,如Valgrind,FreeRTOS中的vTaskList
最后一类是静态分析工具。这一类以polyspace code prover为代表。

相关文章链接
Lauterbach Trace32
https://blog.csdn.net/m0_56208280/article/details/129056690
ARM Keil MDK的使用
https://aijishu.com/a/1060000000426614

polyspace code prover对堆栈的统计

前面三类堆栈统计工具,在统计堆栈使用的时候,其需要编译并运行代码,这意味着其需要定义测试激励。这也意味着,这种情况的堆栈统计是特定测试激励的,其他时候的堆栈大小则需要定义合适的测试激励。而要统计软件的最大堆栈需求,则需要设计合适的测试用例,以运行到最大使用的分支和调用。这无疑对测试用例有比较高的要求。

形式化工具Polyspace Code Prover使用抽象解释法,能够深入探测到每一层函数的调用,统计每个函数本身的局部变量消耗和因为函数调用需要的栈消耗。另外,由于形式化的方法的使用,code prover能够分析代码中的分支是否因为上下文的原因不可达。这也会影响到实际程序中堆栈的大小。

函数的最大最小局部变量使用。函数中或者条件分支中定义的局部变量可能占用的内存。
函数的最大最小堆栈使用。当函数有调用时,除了局部变量外,调用的参数等也会被统计在内;
程序的最大最小堆栈使用。当程序中有main函数或者其他的入口函数,polyspace 可以统计主入口函数的总资源,其包括了调用其他函数需要的资源。
在这里插入图片描述
polyspace能够提供函数的调用关系图,据此可以看到一个函数的入口占用的资源是由于其调用了哪些函数带来的。
在这里插入图片描述
如上图我们知道入口函数ps_main调用了SysTick_Handler函数,也就是堆栈使用量31是SysTick_Handler调用引发的。
在这里插入图片描述
在这里插入图片描述

转到SysTick_Handler也能看到的确如此。
在这里插入图片描述

更复杂的在scheduler_executive调用,从下面调用图看到,其调用了多个函数,而虚的三角型则代表是通过函数指针这类方式进行非显式调用的。
在这里插入图片描述

那么如何知道各个调用的函数的资源开销呢,在上图点击转到定义,然后可以立刻查看该函数的堆栈使用
函数的堆栈统计

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

从上面几个图可以很明显的看到,update_shared_variables占用了最多的资源。随后我们可以继续往下跟踪。

堆栈优化的方式和方法

堆栈优化是一个复杂的过程,需要综合考虑程序的功能、性能和资源限制。以下是一些常见的堆栈优化方法:

  1. 减少局部变量的使用:局部变量存储在堆栈中,减少局部变量的数量可以有效减少堆栈的使用。
  2. 优化函数调用:减少不必要的函数调用深度,避免递归调用,可以减少堆栈的消耗。
  3. 使用堆内存(Heap):对于一些大型的数据结构,可以考虑使用堆内存而不是堆栈来存储,以减轻堆栈的负担。
  4. 静态分配:尽可能使用静态分配的全局变量,减少动态分配在堆栈上的局部变量。
  5. 编译器优化:利用编译器提供的优化选项,如函数内联(inline)等,可以减少函数调用时的堆栈使用。

嵌入式软件堆栈的分配和改进

在嵌入式系统中,堆栈的分配通常在系统初始化阶段完成,需要根据应用的具体需求来合理配置堆栈的大小。过大的堆栈会浪费宝贵的系统资源,而过小的堆栈则可能导致堆栈溢出。因此,合理的堆栈大小评估和动态调整机制对于嵌入式系统的稳定运行至关重要。

  1. 静态分析:在软件开发初期,可以通过静态分析工具预估堆栈的最大使用量,作为堆栈分配的参考。
  2. 动态监测:在软件运行时,通过动态监测工具实时跟踪堆栈的使用情况,及时发现和解决堆栈不足的

这些示例说明了在设计和开发软件时,合理管理和监控堆栈使用的重要性,尤其是在那些对安全性和可靠性要求极高的领域。避免堆栈不足或溢出的策略包括合理分配堆栈大小、避免深层递归调用、使用堆栈监控工具进行动态检查等。

参考资料

你对动态内存分配有多少了解呢
深入分析MCU堆栈的作用,以及该如何设置堆栈大小
IAR等分配堆栈的方式

  • 13
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值