文章目录
- 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
命令来解决重复定义