链接脚本与内存布局:Bootloader的“地图”与“地基”

链接脚本,说白了就是给编译器看的一张“地图”。它告诉编译器:你的代码该放哪,数据该放哪,堆栈又该划在哪块区域。我刚开始做Bootloader移植时,总觉得这玩意儿就是个配置文件,随便改改就行。直到有一次,我把堆栈区定义到了Flash上……嗯,那次调试经历让我终身难忘。

今天我们就来聊聊这张“地图”该怎么画,以及为什么它这么重要。

链接脚本到底在干什么?

你想想看,我们写C代码时,函数、变量、数组都是逻辑上的存在。但芯片可不管这些,它只认地址。链接脚本就是那个“翻译官”,把逻辑上的代码段、数据段,映射到物理的Flash和RAM地址上。

一个典型的Bootloader链接脚本,至少要干这几件事:

  • 定义目标芯片的内存起始地址和大小——比如Flash从0x08000000开始,大小512KB;RAM从0x20000000开始,大小128KB。
  • 划分各个段(Section)的位置——.text放哪,.data放哪,.bss放哪。
  • 定义堆栈的起始地址和大小——这个特别关键,堆栈溢出是Bootloader最常见的死机原因之一。
  • 提供符号给启动代码使用——比如_sstack、_estack、_sidata这些,启动汇编代码要靠它们来初始化。

核心观点:链接脚本不是随便写写的,它直接决定了Bootloader能不能跑起来,以及跑得稳不稳。我见过太多工程师花一周调Bug,最后发现是链接脚本里一个地址写错了。

Flash分区:代码的“永久居留地”

Flash在Bootloader里主要放三样东西:中断向量表、Bootloader自身代码、以及一些只读的配置参数。我个人习惯把Flash分成三个区:

分区 起始地址 大小 内容
向量表区 0x08000000 0x400 (1KB) 中断向量表,必须4字节对齐
Bootloader代码区 0x08000400 0x3FC00 (~255KB) Bootloader主程序、驱动、升级逻辑
参数存储区 0x08040000 0x400 (1KB) 启动标志、版本号、校验值等

为什么要留1KB给向量表?因为ARM Cortex-M系列的向量表必须对齐到2的整数次幂,且大小是2的N次方。1KB是最保险的选择。我曾经见过有人只留了256字节,结果芯片复位后直接跑飞——向量表没对齐。

RAM分区:数据的“临时工棚”

RAM比Flash金贵多了,又小又容易出错。Bootloader里的RAM主要分三块:

  • .data段:存放已初始化的全局变量和静态变量。这些变量在Flash里存着初始值,启动时拷贝到RAM里。
  • .bss段:存放未初始化的全局变量和静态变量。启动时清零。
  • 堆栈区:包括堆(Heap)和栈(Stack)。栈向下生长,堆向上生长。

这里有个坑:很多芯片的RAM起始地址并不是0x20000000,而是有偏移的。比如有些芯片把RAM分成了TCM和SRAM两块,地址不连续。我建议你仔细看芯片参考手册的“Memory Map”章节,别想当然。

我的习惯:在链接脚本里显式定义RAM的起始和结束地址,然后用符号暴露给C代码。这样在代码里可以用extern声明来获取这些地址,做内存边界检查特别方便。

堆栈定义:Bootloader的“生命线”

堆栈溢出是Bootloader最常见的死法,没有之一。为什么?因为Bootloader通常运行在受限环境里,没有操作系统帮你管理栈空间。你调一个函数,递归深了,或者局部变量大了,栈就爆了。

在链接脚本里定义堆栈,我一般这么做:

/* 栈定义 */
_estack = ORIGIN(RAM) + LENGTH(RAM);  /* 栈顶,指向RAM最高地址 */
_sstack = _estack - 0x1000;           /* 栈底,预留4KB给栈 */

/* 堆定义 */
_heap_start = _sstack - 0x0800;       /* 堆起始,在栈下方预留2KB */
_heap_end = ORIGIN(RAM) + 0x2000;     /* 堆结束,限制堆大小 */

为什么栈顶要指向RAM最高地址?因为栈是向下生长的。你把栈顶设在最高处,栈向下扩展时就不会踩到.data和.bss段。这个设计我在多个项目里验证过,很稳。

注意:堆和栈之间一定要留足够的“安全距离”。我曾经在一个项目里把堆和栈紧挨着放,结果堆分配了一块内存,栈一压下去,直接覆盖了堆的数据。那个Bug查了我整整两天。

一个完整的链接脚本示例

下面是我常用的一个Bootloader链接脚本模板,基于STM32F4系列:

ENTRY(Reset_Handler)

MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 256K
  RAM   (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
}

SECTIONS
{
  .isr_vector :
  {
    . = ALIGN(4);
    KEEP(*(.isr_vector))
    . = ALIGN(4);
  } > FLASH

  .text :
  {
    . = ALIGN(4);
    *(.text)
    *(.text*)
    *(.glue_7)
    *(.glue_7t)
    *(.rodata)
    *(.rodata*)
    . = ALIGN(4);
    _etext = .;
  } > FLASH

  _sidata = LOADADDR(.data);

  .data :
  {
    . = ALIGN(4);
    _sdata = .;
    *(.data)
    *(.data*)
    . = ALIGN(4);
    _edata = .;
  } > RAM AT> FLASH

  .bss :
  {
    . = ALIGN(4);
    _sbss = .;
    *(.bss)
    *(.bss*)
    *(COMMON)
    . = ALIGN(4);
    _ebss = .;
  } > RAM

  /* 堆栈定义 */
  ._user_heap_stack :
  {
    . = ALIGN(8);
    PROVIDE ( _end = . );
    PROVIDE ( _heap_start = . );
    . = . + _Heap_Size;
    . = . + _Stack_Size;
    . = ALIGN(8);
    PROVIDE ( _estack = . );
  } > RAM
}

这个脚本里,我特别强调几个点:

  • _sidata:这是.data段在Flash里的加载地址,启动代码要用它来把初始值拷贝到RAM。
  • AT> FLASH:表示.data段运行时在RAM,但初始值存储在Flash里。
  • PROVIDE:这些符号如果没有在其他地方定义,就使用这里的值。这样C代码可以直接引用。

内存布局的SVG结构图

下面这张图展示了典型的Bootloader内存布局,你可以看到Flash和RAM各自的分区,以及堆栈的生长方向:

Bootloader内存布局结构图 Bootloader 内存布局结构图 Flash 存储器 中断向量表 0x08000000 - 0x080003FF Bootloader 代码区 .text + .rodata 0x08000400 - 0x0803FFFF 参数存储区 0x08040000 - 0x080403FF RAM 存储器 .data 段(已初始化数据) 从Flash拷贝初始值 .bss 段(未初始化数据) 启动时清零 堆(Heap) 向上生长 动态内存分配 栈(Stack) 向下生长 函数调用、局部变量 堆向上 栈向下 注意:堆和栈之间必须保留安全距离,防止相互覆盖 栈顶指向RAM最高地址,栈向下生长

避坑指南:我踩过的那些坑

做Bootloader移植这么多年,我在链接脚本上栽过的跟头不少。分享几个典型的:

  • 向量表对齐问题:有一次我把向量表放在0x08000200,结果芯片复位后直接跑飞。后来查手册才发现,Cortex-M4要求向量表必须对齐到2的N次方,且N不小于4。0x200不是2的N次方,所以挂了。
  • 堆栈大小估计不足:Bootloader里调用了printf函数,这个函数内部用了不小的栈空间。我一开始只给了1KB栈,结果一调用printf就死机。后来改成4KB,问题解决。
  • .data段拷贝顺序:启动代码里拷贝.data段时,如果源地址和目标地址有重叠,拷贝会出错。我建议用memcpy而不是自己写循环,因为memcpy处理了重叠情况。

重要提醒:每次修改链接脚本后,一定要检查生成的.map文件。看看各个段的地址对不对,大小合不合理。我习惯在.map文件里搜索“_estack”和“_sstack”,确认栈顶和栈底的值符合预期。

总结一下

链接脚本和内存布局,是Bootloader移植的“地基”。地基没打好,上面盖的房子再漂亮也得塌。我个人建议你在开始写Bootloader代码之前,先把链接脚本写好,把内存布局画清楚。哪怕多花半天时间,也比后面调Bug花一周强。

记住几个关键点:Flash分区要留足向量表空间,RAM分区要保证堆栈安全距离,链接脚本里的符号要和启动代码对应好。做到这三点,你的Bootloader就成功了一半。