当前位置: 首页 > news >正文

研究嵌入式软件架构时遇到的初始化堆栈溢出问题

文章目录

  • 2025年4月10日新增
    • 分析PC寄存器指针值排查问题
    • map文件设计到的知识点
      • 1. **.bss 段(Block Started by Symbol)**
      • 2. **.data 段**
      • 3. **.text 段**
      • 4. **.heap 段**
      • 5. **.stack 段**
      • 6. **.rodata 段(只读数据段)**
      • 7. **.init 和 .fini 段**
      • 8. **符号信息(符号表)**
      • 总结
  • 如题
  • 如何解决?
  • 涉及到的知识点
  • 记录遇到的坑


2025年4月10日新增

首先程序框架已经解决所有问题,可以完美运行了!今天从git上download下来昨天的出错版本,重新分析下昨天的错误程序

在这里插入图片描述
堆栈设置的是1K空间
在这里插入图片描述
这个uart_init()函数里面的buff数组太大导致了堆栈溢出,昨天出了好几种堆栈溢出的情况,这是其一,那个systick初始化的问题我今天没复现,忘记昨天啥情况了,上面即使uart_inti()函数不调用,但是只要上面定义了还是会栈溢出,为什么呢?
在这里插入图片描述
左边error的RW多了0.25k空间,多出来的主要是ZI Data数据
在这里插入图片描述
发现error的map文件中,出现了stdio_streams.o,这个跟printf()函数有关
在这里插入图片描述
可以看出跟uart_init()函数没有关系
在这里插入图片描述
注释了这行代码就解决问题了
在这里插入图片描述
分析两个程序的内存布局差异及跑飞原因
一、内存布局差异对比
​​堆栈空间分配​​
​​异常程序​​:
STACK 段起始地址 0x20000500,大小 1024 bytes
HEAP 段起始地址 0x20000300,大小 512 bytes
​​正常程序​​:
STACK 段起始地址 0x200003f8,大小 1024 bytes
HEAP 段起始地址 0x200001f8,大小 512 bytes
​​差异​​:正常程序的堆栈和堆地址更紧凑,可能是通过调整启动文件中的 Stack_Size 和 Heap_Size 宏定义优化了内存分布。
​​全局变量区域(.bss/.data)​​
​​异常程序​​的 .bss 段存在多模块重叠分配(如 hal_uart1.o 和 stdio_streams.o),导致 active_config 等变量地址冲突(如 0x20000190 和 0x20000078)。
​​正常程序​​的 .bss 段地址分配连续且无重叠,例如 hal_uart1.o 的 rx_buffer 从 0x2000007c 开始,与前后模块无冲突。
二、跑飞原因推测
​​堆栈溢出​​
​​异常程序​​的堆栈起始地址 0x20000500 与 .bss 段末端 0x200002a0 间隔仅 0x260 bytes(约 608 字节),若存在深层次函数调用或中断嵌套,可能超过栈容量导致数据覆盖。
​​正常程序​​的堆栈起始地址 0x200003f8 与 .bss 末端 0x20000198 间隔 0x260 bytes,但通过优化内存分布避免了关键变量被覆盖。
​​内存访问越界​​
​​异常程序​​中 hal_uart1.o 的 tx_buffer 分配了 132 bytes,但其后的 active_config 地址 0x20000190 可能被越界写入(如环形缓冲区未正确管理),破坏相邻数据。
​​中断嵌套未考虑​​
若异常程序中存在高频中断(如 UART 接收中断),且未预留足够中断栈空间(STM32 默认使用主堆栈 MSP),嵌套中断可能耗尽 1024 bytes 的堆栈容量。

分析PC寄存器指针值排查问题

在这里插入图片描述
在这里插入图片描述
PC起始地址是0x8000240,是SystemInit
在这里插入图片描述

在这里插入图片描述
单步执行后发现PC指针跑飞
在这里插入图片描述
实际上,我这里是重定向出了问题

//int fputc(int ch, FILE *f)
//{
//	while (0 == (USART1->SR & 0X40));//	USART1->DR = (uint8_t)ch;//	return ch;
//}#if 1
#pragma import(__use_no_semihosting)// 使用#pragma指令来导入__use_no_semihosting,表示不使用半主机模式。
// 半主机模式通常用于调试,允许在目标设备上运行时与主机进行交互。
struct __FILE
{int handle;
};FILE __stdout;// 定义一个全局变量__stdout,类型为FILE,用于标准输出。
void _sys_exit(int x)
{x = x;
}int fputc(int ch, FILE *f)
{while (0 == (USART1->SR & 0X40));USART1->DR = (uint8_t)ch;return ch;
}
#endif

参考正点原子这样修改解决了问题

map文件设计到的知识点

在分析嵌入式系统或单片机的 map 文件时,map 文件提供了详细的内存布局和各个段(section)在内存中的分配情况。map 文件的内容对调试、优化和了解程序结构非常有帮助。

下面是对 map 文件中常见的几个段(例如 .bss 段、.data 段以及其他段)的详细分析。

1. .bss 段(Block Started by Symbol)

  • 描述.bss 段是用于未初始化的全局变量和静态变量的内存区域。在程序启动时,这些变量会被清零或初始化为零。
  • 特点
    • .bss 段通常不占用实际的存储空间。在编译时,链接器并不为 .bss 段中的变量分配实际的存储空间,而是指示程序运行时为这些变量分配空间,并在程序加载时清零它们。
    • 它通常包括未显式初始化为特定值的静态变量和全局变量。
  • 例子
    int global_var;  // 默认值为 0
    static int static_var;  // 默认值为 0
    
  • map 文件中的信息
    • map 文件中,.bss 段会列出所有未初始化的全局和静态变量及其所占的内存空间大小。由于这些变量会在程序启动时被清零,因此 .bss 段通常不会占用磁盘上的存储空间。
    • 大小map 文件中 .bss 段的大小通常会较大,尤其是未初始化变量较多时。

2. .data 段

  • 描述.data 段是用于存储已初始化的全局变量和静态变量的内存区域。与 .bss 段不同,.data 段中的变量在编译时就已经被赋予了初始值。
  • 特点
    • .data 段包含所有显式初始化的全局和静态变量。
    • 在程序启动时,.data 段的内容会从程序的可执行文件(或固件)加载到内存中。
  • 例子
    int global_var = 10;  // 已初始化的全局变量
    static int static_var = 20;  // 已初始化的静态变量
    
  • map 文件中的信息
    • map 文件会列出 .data 段中所有已初始化变量及其在内存中的起始地址和大小。
    • 大小.data 段的大小取决于已初始化的变量总量。

3. .text 段

  • 描述.text 段是程序的代码段,它存放了编译后的机器代码。在这个段中没有变量,只有程序的指令。
  • 特点
    • 代码段通常是只读的,因为程序代码在执行时不应该被修改。
    • .text 段通常是程序中占用空间最多的部分,尤其是复杂程序和函数较多时。
  • map 文件中的信息
    • map 文件会显示 .text 段的起始地址、大小以及每个函数的实际内存位置。
    • 大小:代码段的大小会受到程序中函数和指令的复杂度影响。

4. .heap 段

  • 描述.heap 段用于动态分配内存(例如通过 malloc, calloc, new 等函数分配的内存),通常位于程序的栈(.stack)段之后。
  • 特点
    • 动态内存分配的内存区域。
    • 程序运行时的内存分配和释放会发生在此区域。
  • map 文件中的信息
    • .heap 段的内存大小取决于程序运行时实际分配的内存量。
    • map 文件会显示 .heap 段的起始地址、大小等信息。

5. .stack 段

  • 描述.stack 段用于存储函数调用时的局部变量、返回地址等信息。栈是一个动态分配的内存区域,随着函数的调用和返回不断增长和缩小。
  • 特点
    • 栈的大小是由编译器或链接器预设的,通常在 map 文件中可以看到栈的大小。
    • 栈是一个向下增长的内存区域,通常在内存地址空间较高的位置。
  • map 文件中的信息
    • map 文件会显示栈的起始位置和大小等信息。栈的大小通常在 linker script 或编译器设置中配置。

6. .rodata 段(只读数据段)

  • 描述.rodata 段用于存储程序中的常量数据(如字符串字面量、常量数组等),这些数据在程序运行期间不会被修改。
  • 特点
    • .rodata 是只读的,不允许修改其中的数据。
    • 通常存储字符串常量、全局常量等。
  • map 文件中的信息
    • map 文件会列出 .rodata 段的内存占用情况,包括字符串常量等的内存地址和大小。

7. .init 和 .fini 段

  • 描述
    • .init 段包含程序初始化代码,在程序启动时被调用,通常用于设置初始化操作,如硬件初始化等。
    • .fini 段包含程序结束时的清理代码,通常在程序退出前进行资源清理。
  • 特点
    • 这两个段通常由编译器或链接器自动处理。
  • map 文件中的信息
    • map 文件会显示 .init.fini 段的地址和大小。

8. 符号信息(符号表)

  • 描述map 文件中还包含符号表,列出了程序中的所有符号(如变量、函数等)及其在内存中的地址。
  • 特点
    • 符号表包含了所有变量和函数的名称、类型和内存地址等信息,通常用于调试。
  • map 文件中的信息
    • 每个符号(如全局变量、静态变量、函数等)都会有一个对应的内存地址和大小。

总结

map 文件中,除了 .bss.data.text 段外,还包含了栈、堆、只读数据段等信息。每个段都有不同的作用,且在编译和链接过程中被分配到不同的内存区域。通过查看 map 文件,你可以清晰地了解程序的内存分布、变量和函数的位置,进而优化内存使用和提高程序性能。

如题

//hal_systick_config_t cfg =
//{
//    .clk_source = SYSTICK_CLK_HCLK_DIV8,
//    .reload_value = 21000,
//    .sys_clk_frequency = 168,
//    .tick_callback = timer_ticks_count
//};static void systick_init(void)
{hal_systick_config_t cfg ={.clk_source = SYSTICK_CLK_HCLK_DIV8,.reload_value = 21000,.sys_clk_frequency = 168,.tick_callback = timer_ticks_count};hal_systick_init(&cfg);
}

这个代码中,只要cfg结构体变量放到函数外面一切正常,只要放在函数内部则进入debug模式后需要三次run之后程序才会执行,把栈空间调大依然会出现这个问题

如何解决?

在这里插入图片描述

涉及到的知识点

一、堆栈初始值计算公式
STM32 的堆栈起始地址(即 SP 初始值)由以下公式决定:

SP_初始值=SRAM起始地址+RW_Data大小+ZI_Data大小+Stack_Size
​​SRAM起始地址​​:STM32F407 的 SRAM 起始地址为 0x20000000
​​RW_Data(可读写数据)​​:已初始化的全局变量和静态变量
​​ZI_Data(零初始化数据)​​:未初始化的全局变量和静态变量(默认填充为0)
​​Stack_Size​​:启动文件中定义的栈大小(您设置为 0x400,即 1KB)
二、现象解释
在您的案例中,​​SP 初始值为 0x20000900​​ 的具体计算逻辑如下:

​​默认内存布局​​:
SRAM 起始地址:0x20000000
程序中的全局变量(RW+ZI)总大小:假设为 0x900 字节
栈大小(Stack_Size):0x400 字节(1KB)
则 SP 初始值 = 0x20000000 + 0x900 + 0x400 = 0x20000D00
但实际观察到的 SP 值为 0x20000900,这表明 ​​RW+ZI 实际仅占用 0x500 字节​​(0x20000900 - 0x20000000 - 0x400 = 0x500)。
​​堆栈生长方向​​:
STM32 的栈是 ​​向下生长​​ 的,SP 初始值指向栈顶(高地址),栈底为 SP - Stack_Size。因此:
栈顶地址:0x20000900
栈底地址:0x20000900 - 0x400 = 0x20000500
栈空间范围:0x20000500 ~ 0x20000900。

根据.map文件分析,可以得出以下关于栈溢出问题的结论:

一、栈空间配置分析
​​栈大小设置​​
在startup_stm32f40_41xxx.o中明确显示:
STACK 0x20000500 0x00000400 (1KB)
这与用户设置的0x400(1024字节)完全一致。
​​内存布局验证​​
栈的地址范围:0x20000500 ~ 0x20000900
堆的地址范围:0x20000300 ~ 0x20000500(512字节)
全局变量(ZI + RW Data):总占用2304字节(RW_IRAM1区域)。
二、栈溢出风险判断
​​栈使用量估算​​
根据Image component sizes,ZI Data(零初始化数据)为2268字节,RW Data为36字节,总占用2304字节。
​​关键点​​:ZI Data包含全局变量和静态变量,而栈和堆的地址范围与ZI/RW区域相邻。如果函数调用链中的局部变量过多,可能导致栈指针(SP)超出0x20000900,覆盖其他内存区域。
​​调试现象关联​​
用户提到“变量放在函数内部时需要多次运行才成功”,这与栈溢出的典型现象(随机性崩溃、HardFault)高度吻合。局部变量存储在栈中,若超过0x400限制,会破坏中断向量表或关键数据,导致程序异常。
​​溢出检测方法​​
​​静态分析​​:检查函数调用层级和局部变量总大小。例如,若某函数定义了char buffer[1024],直接占满栈空间,必然溢出。
​​动态验证​​:在Keil调试器中观察SP寄存器值是否超出0x20000900,或通过Memory窗口查看栈内存是否被意外改写。

记录遇到的坑

1、函数指针一定不能跑飞,加上保护,防止程序跑飞
2、局部变量或者局部数据,尤其局部大数组一定要static化,或者直接全局化,防止堆栈溢出
3、利用三元运算符替代if逻辑,程序看起来简洁优雅
4、巧用#ifndef HAL_STATUS_T_DEFINED
#define HAL_STATUS_T_DEFINED
命令来解决重复定义


http://www.mrgr.cn/news/97819.html

相关文章:

  • 3 版本控制:GitLab、Jenkins 工作流及分支开发模式实践
  • LeetCode Hot100 刷题笔记(2)—— 子串、普通数组、矩阵
  • 【回眸】Linux 内核 (十六) 之 多线程编程 下
  • 中间件-消息队列
  • C# 设置Excel中文本的对齐方式、换行、和旋转
  • 【HTML】纯前端网页小游戏-戳破彩泡
  • IDEA :物联网ThingsBoard-gateway配置,运行Python版本,连接thingsboard,接入 MQTT 设备
  • 以普通用户身份启动pure-ftpd服务端
  • js chrome 插件,下载微博视频
  • 蓝桥杯备赛学习笔记:高频考点与真题预测(C++/Java/python版)
  • 开源的7B参数OCR视觉大模型:RolmOCR
  • 【论文精读】Multi-scale Neighbourhood Feature Interaction Network
  • windows使用cmake安装openvdb-12.0.0库
  • IDEA遇到问题汇总
  • 【UE5】RTS游戏的框选功能+行军线效果实现
  • 机器学习之PCA主成分分析详解
  • 面试如何应用大模型
  • 算法训练之动态规划(一)
  • 在 Jupyter Notebook 中使用 Pandas 进行数据操作
  • 【语法】C++的list