4、静态内存分配:全局变量、静态变量、常量区,以及链接脚本(.ld)的作用
好,咱们今天聊点实在的。静态内存分配,说白了就是那些「从程序启动到结束,一直占着位置不走」的内存。你想想看,全局变量、静态变量、常量,它们都住在这个区域里。我刚开始做嵌入式那会儿,总觉得这块没啥好学的——不就是编译器帮我安排好了吗?直到有一次,我在一个资源紧张的MCU项目上,因为没搞懂链接脚本,硬生生把程序搞崩了……嗯,从那以后,我再也不敢小看这块内容了。
4.1 全局变量与静态变量:它们到底住在哪?
先说说全局变量。你在函数外面定义的变量,比如:
int g_counter = 0; // 全局变量
static int s_flag = 1; // 静态全局变量
这两个家伙,都待在「静态存储区」里。区别在哪?g_counter 整个程序都能访问,s_flag 只有当前文件能碰。但它们的生命周期是一样的——从程序启动到结束,一直活着。
我个人习惯把全局变量和静态变量统称为「静态数据」。它们又分两类:
- 已初始化的:比如
int g_val = 10;,存放在.data段 - 未初始化的:比如
int g_buf[256];,存放在.bss段
这里有个坑,我遇到过好几次。有些新手以为未初始化的全局变量默认就是0,这没错。但问题是,.bss 段在程序映像里是不占空间的!它只在运行时被清零。如果你在链接脚本里忘了给 .bss 分配足够的RAM空间,那程序一跑起来就乱套了。
.bss 段的大小一定要在链接脚本里明确指定。
4.2 常量区:只读的「铁饭碗」
常量区,也叫只读数据区,存放的是 const 修饰的变量和字符串常量。比如:
const char* msg = "Hello RTOS";
const int table[4] = {1, 2, 3, 4};
这些数据,程序运行期间不能改。你敢改?MCU直接给你来个 HardFault。为什么?因为常量区通常被映射到Flash或者ROM里,硬件上就不让你写。
你可能会问:「那 const 变量和普通全局变量有啥区别?」
区别大了去了。普通全局变量在RAM里,可以随时改。常量在Flash里,改不了。这直接影响了你的内存占用。我见过有人把一个大数组定义成 const,结果RAM省下来一大块。说白了,能用 const 就用,别客气。
const 定义。这样它们会待在Flash里,不占宝贵的RAM空间。
4.3 链接脚本(.ld):内存的「总设计师」
好,现在到了重头戏——链接脚本。很多嵌入式开发者觉得这玩意儿是编译器的事,跟自己没关系。我刚开始也这么想,直到有一次我需要把一段关键代码放到SRAM里运行……
链接脚本,说白了就是告诉链接器:「你的代码放哪,数据放哪,堆栈放哪。」它长这样:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS
{
.text :
{
*(.text*)
*(.rodata*)
} > FLASH
.data :
{
*(.data*)
} > RAM AT> FLASH
.bss :
{
*(.bss*)
} > RAM
}
看到没?MEMORY 定义了硬件的内存布局——Flash从哪开始,RAM从哪开始。 SECTIONS 则把不同的数据段分配到对应的内存区域。
这里有个关键点:.data 段为什么有两处地址?> RAM AT> FLASH 的意思是:运行时在RAM里,但初始值存在Flash里。程序启动时,启动代码会把 .data 从Flash拷贝到RAM。这就是所谓的「加载时地址」和「运行时地址」的区别。
我记得有一次调试一个Bootloader项目,发现全局变量全是乱的。查了半天,原来是启动代码里忘了拷贝 .data 段。嗯,从那以后,我每次写链接脚本都会反复确认这个拷贝逻辑。
4.4 静态内存分配在RTOS中的实战意义
在RTOS里,静态内存分配尤其重要。为什么?因为动态分配(malloc)在嵌入式系统里是「高危操作」。你想想看,一个实时任务正在跑,突然malloc失败返回NULL,整个系统可能就崩了。
所以很多RTOS内核,比如FreeRTOS,都推荐用静态方式创建任务:
static StackType_t task_stack[1024];
static TaskHandle_t task_handle;
void create_task(void)
{
xTaskCreateStatic(
task_function,
"MyTask",
1024,
NULL,
1,
task_stack,
&task_handle
);
}
这样,任务栈是静态分配的,在编译时就确定了位置。运行时不会出现分配失败的情况。我个人习惯在项目里尽量用静态分配,除非是那些确实需要动态创建/销毁的场景。
- 全局变量和静态变量在 .data(已初始化)和 .bss(未初始化)段
- 常量在 .rodata 段,通常放在Flash里
- 链接脚本决定了这些段在内存中的具体位置
- RTOS任务栈建议用静态分配,避免运行时风险
4.5 一个实战案例:链接脚本配置错误导致的内存溢出
最后,分享一个我亲身经历的案例。有一次做一款智能家居网关,MCU是STM32F407,RAM有192KB。我在链接脚本里把堆(Heap)设成了64KB,栈(Stack)设成了16KB,剩下的给全局变量。
结果程序跑起来后,只要一调用 malloc 分配大块内存,系统就死机。我一开始以为是堆碎片问题,折腾了好久。后来仔细一看链接脚本——我把 .bss 段和堆的区域重叠了!全局变量和堆共用了一块内存,不出问题才怪。
从那以后,我每次写完链接脚本都会做一件事:手动计算每个段的大小,确保它们不重叠。你可以用 arm-none-eabi-size 工具查看编译后的段大小:
$ arm-none-eabi-size my_program.elf
text data bss dec hex
45232 1024 8192 54448 d4b0
看到 data 和 bss 的大小了吗?这些就是你要在链接脚本里分配的空间。别偷懒,算一算,心里有底。
好了,这一章就聊到这。静态内存分配看似简单,但里面的门道不少。下一章我们聊聊动态内存分配——那个更刺激,也更危险的话题。