第一章 链接脚本与内存布局

大家好,我是你们的老朋友。做了十几年嵌入式底层开发,从单片机到游戏机系统,我踩过的坑比走过的路还多。今天咱们聊聊链接脚本和内存布局——这玩意儿看着枯燥,但你要是搞不定它,Bootloader 根本跑不起来。

说白了,链接脚本就是告诉编译器:「你的代码放哪儿,数据放哪儿,栈和堆又该在哪儿安家。」没有它,你写再多代码也是白搭。

1.1 链接脚本语法入门

先看一个最简单的链接脚本骨架。我个人习惯从 ENTRY 开始写,因为它定义了程序的第一条指令。

ENTRY(_start)

MEMORY
{
    ROM (rx)  : ORIGIN = 0x00000000, LENGTH = 512K
    RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}

SECTIONS
{
    .text : {
        *(.text*)
    } > ROM

    .data : {
        *(.data*)
    } > RAM AT> ROM

    .bss : {
        *(.bss*)
    } > RAM
}

嗯,这里要注意三个关键字:

  • ENTRY:指定入口点。我见过有人忘了写,结果程序从地址 0 开始跑,直接飞了。
  • MEMORY:定义物理内存区域。ROM 是只读的,RAM 可读写。长度别写错,写大了链接器不报错,但运行时必挂。
  • SECTIONS:把代码和数据分配到各个段里。

重要提醒AT> ROM 表示 .data 段在 ROM 里存储,运行时拷贝到 RAM。这个机制叫「加载时地址 vs 运行时地址」,很多新手在这里翻车。

1.2 代码段、数据段、BSS段布局

咱们拆开来看每个段是干嘛的。

段名 内容 存放位置 特点
.text 可执行指令、常量字符串 ROM/Flash 只读,掉电不丢失
.data 已初始化的全局变量 运行时在 RAM,镜像在 ROM 需要从 ROM 拷贝到 RAM
.bss 未初始化的全局变量 RAM 启动时清零,不占 ROM 空间
.rodata 只读数据(const 变量) ROM 和 .text 常合并

我在项目中遇到过一个问题:.bss 段忘记清零,结果全局变量初始值全是随机数。那次调试花了我整整两天。后来我养成了习惯——在 Bootloader 里手动清零 .bss。

我的小技巧:在链接脚本里定义两个符号 __bss_start__bss_end,然后在 C 代码里用 memset 清零。这样既清晰又可靠。

1.3 栈与堆的规划

栈和堆是运行时最活跃的区域,但链接脚本里通常只给它们划个范围。具体怎么用,得靠启动代码来初始化。

先看栈。我个人习惯把栈放在 RAM 的最高地址,因为栈是向下生长的。你想想看,如果栈放在中间,一不小心就踩到堆或者数据段了。

_stack_top = ORIGIN(RAM) + LENGTH(RAM);
_stack_size = 4K;  /* 根据需求调整 */

堆呢?一般放在 .bss 后面。游戏机系统里,堆主要用于动态分配纹理、音频缓冲区等。但 Bootloader 阶段我建议少用堆,能静态分配就静态分配。

避坑指南:我曾经在一个项目里把栈设得太小,结果函数调用深一点就栈溢出,系统随机死机。排查了三天才发现是栈不够用。记住:中断嵌套、递归调用都会吃栈,留足余量!

1.4 实战:一个游戏机 Bootloader 的内存布局

假设咱们在做一台掌机,CPU 是 Cortex-M7,Flash 2MB,RAM 512KB。我会这样规划:

MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 2M
    RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 512K
}

SECTIONS
{
    /* 中断向量表必须放在开头 */
    .isr_vector : {
        *(.isr_vector)
    } > FLASH

    /* 代码段 */
    .text : {
        *(.text*)
        *(.rodata*)
    } > FLASH

    /* 初始化数据 */
    .data : {
        _sdata = .;
        *(.data*)
        _edata = .;
    } > RAM AT> FLASH

    /* BSS 段 */
    .bss : {
        _sbss = .;
        *(.bss*)
        _ebss = .;
    } > RAM

    /* 堆 */
    .heap : {
        _heap_start = .;
        . = . + 64K;
        _heap_end = .;
    } > RAM

    /* 栈 */
    .stack : {
        _stack_start = .;
        . = . + 16K;
        _stack_end = .;
    } > RAM
}

为什么栈只给 16K?因为 Bootloader 阶段功能简单,不需要太多栈。等加载完游戏主程序,栈会重新设置。你想想看,省下来的 RAM 可以给游戏用,多划算。

核心要点:链接脚本不是写一次就完事的。每加一个新功能,都要回头看看内存够不够。我习惯在编译后检查 .map 文件,确认每个段的实际大小。

1.5 本章小结

嗯,这一章内容不少。咱们捋一捋:

  • 链接脚本用 ENTRY、MEMORY、SECTIONS 三个关键字定义程序布局
  • .text 放代码,.data 放初始化数据,.bss 放未初始化数据
  • 栈向下生长,放在 RAM 顶部;堆向上生长,放在 .bss 后面
  • Bootloader 里尽量少用堆,栈大小要留余量

下一章咱们会深入启动代码,看看怎么用汇编把 C 环境搭起来。到时候你会看到,链接脚本里定义的符号是怎么在代码里发挥作用的。

记住:内存布局是 Bootloader 的地基。地基没打好,上面盖什么都白搭。