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

做Bootloader开发,绕不开的一个东西就是链接脚本。说实话,我刚入行那会儿,觉得这玩意儿就是个黑盒子——编译器自己会搞定,我干嘛要管它?直到有一次,我写的Bootloader死活跑不起来,调试了整整两天,最后发现是RAM地址配错了。嗯,从那以后,我再也不敢轻视链接脚本了。

4.1 链接脚本到底在干什么?

说白了,链接脚本就是告诉链接器:你的代码该放哪儿,数据该放哪儿,哪些东西要留在Flash,哪些要搬到RAM。你想想看,MCU上电后,Flash里的代码要执行,但变量不能放在Flash里(因为要读写),所以得有个搬运过程。链接脚本就是干这个的。

我个人习惯把链接脚本比作一张「房产证」——它规定了每块内存区域的归属和用途。没有它,编译器就像个无头苍蝇,乱放一气。

核心要点:链接脚本定义了目标文件的段(Section)如何映射到目标硬件的物理内存地址上。

4.2 链接脚本语法速览

GCC用的链接脚本语法,其实没那么复杂。我挑几个最常用的说。

4.2.1 基本结构

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则告诉链接器,每个段该放哪儿。

小技巧:我在项目中经常用 AT> 这个语法。它表示「这个段运行时在RAM,但初始值存在Flash里」。Bootloader启动时,需要自己把数据从Flash拷贝到RAM——这就是所谓的「重定位」。

4.2.2 常用关键字

  • ORIGIN:起始地址。比如Flash从0x08000000开始,这是STM32的典型值。
  • LENGTH:大小。别超了,超了链接器会报错。
  • AT>:加载地址。运行时地址和加载地址可以不同。
  • ALIGN:对齐。我建议你养成对齐的习惯,否则某些外设会罢工。

4.3 代码段、数据段、BSS段

这三个段,是嵌入式开发的老朋友了。但你真的理解它们吗?

4.3.1 .text 段(代码段)

存放可执行指令和只读常量。比如你写的 ifwhile、函数调用,全在这里。还有 const 修饰的变量,也放这里。

特点:只读,必须放在Flash里。你想想看,代码在RAM里跑当然更快,但RAM贵啊,而且掉电就没了。所以Bootloader的代码段,老老实实放Flash。

注意:我曾经遇到一个坑——把中断向量表也放在了.text段里。结果程序跑飞后,中断向量表被意外修改,整个系统崩溃。后来我强制把向量表单独放在一个段里,并加上只读属性,问题才解决。

4.3.2 .data 段(数据段)

存放已初始化的全局变量和静态变量。比如 int count = 100;,这个 100 的初始值存在Flash里,但 count 这个变量本身必须在RAM里,因为程序运行时会修改它。

所以.data段有两个地址:

  • 加载地址(LMA):在Flash里,存放初始值。
  • 运行地址(VMA):在RAM里,变量实际的位置。

Bootloader启动时,必须把.data段从Flash拷贝到RAM。这个拷贝动作,通常写在启动文件(startup.s)里。

4.3.3 .bss 段

存放未初始化的全局变量和静态变量。比如 int buffer[1024];,没有赋初值。这些变量在RAM里,但初始值全是0。

为什么叫bss? 我记得是「Block Started by Symbol」的缩写,老古董了。你只要记住:bss段不占用Flash空间,只占用RAM空间。启动时,需要把这段RAM全部清零。

段名 存储位置 内容 启动时操作
.text Flash 代码、只读常量 无需操作
.data Flash(初始值)+ RAM(运行时) 已初始化全局/静态变量 从Flash拷贝到RAM
.bss RAM 未初始化全局/静态变量 清零

4.4 Flash与RAM分区策略

做Bootloader,分区是个技术活。分不好,轻则空间浪费,重则程序跑飞。

4.4.1 Flash分区

我一般把Flash分成三块:

  1. Bootloader区:存放Bootloader代码。大小通常16KB~64KB,看功能复杂度。
  2. 应用区:存放用户应用程序。这是大头,剩下的Flash基本都给它。
  3. 参数存储区:存放校准参数、配置信息、升级标志等。我习惯放在Flash的最后几页,方便擦写。
/* Flash分区示例 */
#define BOOTLOADER_ADDR     0x08000000
#define BOOTLOADER_SIZE     0x00010000  /* 64KB */

#define APP_ADDR            0x08010000
#define APP_SIZE            0x00070000  /* 448KB */

#define PARAM_ADDR          0x08080000
#define PARAM_SIZE          0x00001000  /* 4KB */

我的经验:Bootloader区最好放在Flash的最开头。因为MCU上电后,默认从0x08000000开始执行。如果你把Bootloader放在别处,还得折腾中断向量表重映射,麻烦得很。

4.4.2 RAM分区

RAM分区相对简单,但有个关键点——中断向量表

有些MCU允许把中断向量表搬到RAM里,这样可以动态修改中断响应。但Bootloader阶段,我建议别这么干。原因很简单:Bootloader的任务是升级程序,不是跑应用。把向量表留在Flash里,稳定可靠。

RAM分区我通常这样分:

  • 栈区:从RAM最高地址往下生长。大小根据函数调用深度定,一般2KB~8KB。
  • 堆区:如果用了malloc,需要预留。但Bootloader里我尽量不用动态内存分配。
  • 数据区:.data和.bss段,从RAM起始地址开始放。

避坑指南:我曾经在某个项目里,把栈大小设得太小,结果函数递归调用时栈溢出了,程序莫名其妙地复位。查了三天才找到原因。后来我养成了一个习惯——在链接脚本里加上栈溢出检测,比如用 __stack_chk_guard 这种机制。

4.5 实战:一个典型的Bootloader链接脚本

下面是我常用的一个模板,你可以直接拿来改:

/* Bootloader链接脚本示例 */
MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 64K
    RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}

_estack = ORIGIN(RAM) + LENGTH(RAM);

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

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

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

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

注意看,我定义了几个符号:_sdata_edata_sbss_ebss。这些符号在启动文件里会被用到,用来实现.data拷贝和.bss清零。

4.6 小结

链接脚本这东西,说难不难,说简单也不简单。你只要记住三点:

  • 代码放Flash,变量放RAM。
  • 已初始化的变量,初始值存在Flash里,启动时搬到RAM。
  • 未初始化的变量,启动时清零。

剩下的,就是根据你的MCU型号和Bootloader大小,调整地址和长度。多试几次,踩几个坑,自然就熟了。

下一章,我们会讲启动流程——也就是Bootloader上电后,到底先干哪件事。到时候你会看到,链接脚本里定义的这些符号,是怎么被用起来的。