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空间,那程序一跑起来就乱套了。

⚠️ 避坑指南:我曾经在一个STM32项目上,因为 .bss 段大小计算错误,导致全局数组越界覆盖了堆栈区。查了整整两天才找到原因。记住:.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 就用,别客气。

💡 小技巧:在RTOS任务里,如果你有一些查表用的数据,比如正弦表、PID参数表,强烈建议用 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

看到 databss 的大小了吗?这些就是你要在链接脚本里分配的空间。别偷懒,算一算,心里有底。

好了,这一章就聊到这。静态内存分配看似简单,但里面的门道不少。下一章我们聊聊动态内存分配——那个更刺激,也更危险的话题。