3. Bootloader核心概念

链接脚本,说白了就是告诉链接器:你的芯片内存长什么样,代码该放哪,数据该放哪。很多做应用层开发的同事可能几年都不碰一次 .ld 文件,但做 Bootloader 开发,这关你必须过。

我记得刚入行那会儿,接手一个 STM32 的 Bootloader 项目,编译通过,下载也正常,一跑就死。折腾了两天,最后发现是链接脚本里 Flash 起始地址写错了,Bootloader 把自己覆盖了。嗯,从那以后,我每次改 .ld 文件都会反复核对三遍。

3.1 链接脚本的基本结构

一个标准的 .ld 文件,通常由三大部分组成:

  • 内存布局定义(MEMORY):描述芯片的物理存储资源
  • 段映射规则(SECTIONS):描述如何将编译产物分配到内存中
  • 符号定义与赋值:提供链接时需要的全局符号

来看一个最简的示例,这是我个人习惯的模板结构:

/* 内存布局定义 */
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
}

你可能会问:为什么 .data 段要写两遍地址?> RAM AT> FLASH 是什么意思?别急,下面我会详细拆解。

3.2 内存布局定义(MEMORY)

MEMORY 块定义了目标芯片的物理内存资源。每个区域需要指定:

属性 含义 常见取值
名称 区域标识符,供 SECTIONS 引用 FLASH, RAM, CCMRAM
属性 访问权限 r(读), w(写), x(执行)
ORIGIN 起始地址 0x08000000(STM32 Flash 起始)
LENGTH 区域大小 512K, 128K, 64K
我曾经踩过的坑: 某国产芯片的 Flash 其实分两个 bank,地址不连续。我按手册写了 ORIGIN 和 LENGTH,结果链接器报地址溢出。后来发现需要把两个 bank 分别定义成两个 MEMORY 区域,然后在 SECTIONS 里手动分配。所以,拿到新芯片一定要先看 Memory Map 章节,别想当然。

3.3 段映射规则(SECTIONS)

SECTIONS 是链接脚本的核心。它告诉链接器:每个段(.text, .data, .bss)应该放在哪个内存区域,以及它们的排列顺序。

3.3.1 .text 段:代码与只读数据

.text 段包含程序的可执行指令和只读常量。它通常放在 Flash 中,因为 Bootloader 的代码不需要修改。

.text :
{
    /* 中断向量表必须放在最前面 */
    . = ALIGN(4);
    KEEP(*(.isr_vector))
    
    /* 程序代码 */
    *(.text*)
    
    /* 只读数据 */
    *(.rodata*)
    
    /* 链接时结束地址 */
    _etext = .;
} > FLASH

这里有个细节:KEEP() 是告诉链接器,即使这个段没有被显式引用,也要保留。中断向量表就是典型例子——它没有函数调用它,但 CPU 复位后会从这里取地址。

3.3.2 .data 段:已初始化的全局变量

.data 段存放已初始化的全局变量和静态变量。这些变量在运行时位于 RAM 中,但初始值必须存储在 Flash 里。这就是为什么需要 > RAM AT> FLASH:

  • VMA(虚拟地址):运行时地址,在 RAM 中
  • LMA(加载地址):存储地址,在 Flash 中
.data : ALIGN(4)
{
    _sdata = .;        /* RAM 中 .data 起始地址 */
    *(.data*)
    _edata = .;        /* RAM 中 .data 结束地址 */
} > RAM AT> FLASH

/* Flash 中 .data 的存储起始地址 */
_sidata = LOADADDR(.data);

Bootloader 启动时,必须把 .data 从 Flash 拷贝到 RAM。这个拷贝操作依赖 _sdata、_edata 和 _sidata 三个符号。我个人习惯在启动文件里这样写:

/* 汇编伪代码 */
ldr r0, =_sidata    /* Flash 源地址 */
ldr r1, =_sdata     /* RAM 目标地址 */
ldr r2, =_edata     /* RAM 结束地址 */
copy_loop:
    cmp r1, r2
    bhs copy_done
    ldr r3, [r0], #4
    str r3, [r1], #4
    b copy_loop
copy_done:

3.3.3 .bss 段:未初始化的全局变量

.bss 段存放未初始化或初始化为 0 的全局变量。它只占用 RAM 空间,不占用 Flash 空间。启动时只需要将对应 RAM 区域清零即可。

.bss : ALIGN(4)
{
    _sbss = .;        /* .bss 起始地址 */
    *(.bss*)
    *(COMMON)
    _ebss = .;        /* .bss 结束地址 */
} > RAM
一个小技巧: 如果你发现程序运行后某些全局变量值不对,先检查 .bss 清零是否执行了。我曾经遇到一个情况,调试器下载后变量值是对的,但复位后就不对了——因为 Bootloader 跳转前没有清 .bss,而应用程序依赖 .bss 初始为 0。嗯,这个问题排查了我整整一个下午。

3.4 段映射的完整流程

下面这张图展示了从编译产物到最终内存布局的完整映射关系:

链接脚本段映射流程 链接脚本段映射流程 编译产物(.o 文件) .text 段(代码+只读数据) .data 段(已初始化全局变量) .bss 段(未初始化全局变量) 链接器(ld) 最终内存布局(.elf) Flash: .text + .rodata Flash: .data 初始值(LMA) RAM: .data + .bss(VMA) 关键说明 1. .text 段直接映射到 Flash,运行时无需搬移 2. .data 段在 Flash 中存储初始值(LMA),启动时拷贝到 RAM(VMA) 3. .bss 段只占用 RAM 空间,启动时清零 4. 链接脚本通过 _sdata/_edata/_sidata/_sbss/_ebss 符号控制搬移

3.5 Bootloader 特有的链接脚本考量

做 Bootloader 时,链接脚本有几个特殊之处需要留意:

  1. 起始地址偏移:Bootloader 通常从 Flash 起始地址开始,但应用程序需要偏移。比如 Bootloader 占 32KB,应用程序的 Flash 起始地址就是 0x08008000。
  2. 中断向量表重定位:Bootloader 和应用程序各自有独立的中断向量表。链接脚本中必须为两者分别保留向量表空间。
  3. RAM 分区:Bootloader 和应用程序可能共享 RAM,也可能各自独占一部分。我建议在链接脚本中明确划分,避免互相踩踏。

实战案例: 某项目需要实现双区 Bootloader(A/B 升级)。我在链接脚本中定义了两个 Flash 区域:

MEMORY
{
    BOOT_FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 32K
    APP_FLASH_A (rx) : ORIGIN = 0x08008000, LENGTH = 240K
    APP_FLASH_B (rx) : ORIGIN = 0x08040000, LENGTH = 240K
    RAM (rwx)        : ORIGIN = 0x20000000, LENGTH = 128K
}

然后通过不同的链接脚本配置,编译出分别对应 A 区和 B 区的固件。升级时,Bootloader 根据标志位决定从哪个区启动。

3.6 常见问题与避坑指南

问题现象 可能原因 解决方案
程序跑飞,复位后无法启动 中断向量表地址不对 检查 .isr_vector 是否放在段首,且地址对齐
全局变量初始值不对 .data 段未正确拷贝 检查 _sidata/_sdata/_edata 符号是否定义正确
全局变量值随机 .bss 段未清零 检查启动代码中是否执行了 .bss 清零
链接时报地址溢出 段大小超过 MEMORY 定义 检查 LENGTH 是否足够,或优化代码大小
我曾经踩过的坑: 有一次我修改了链接脚本,把 .data 段的 VMA 和 LMA 写反了——运行时变量在 Flash 里,写操作直接导致 HardFault。排查过程极其痛苦,因为代码逻辑完全正确,就是地址映射错了。所以,每次改完 .ld 文件,建议用 objdump -h 或 readelf -S 检查一下各段的 VMA 和 LMA 是否符合预期。

嗯,链接脚本这部分内容确实比较枯燥,但它是 Bootloader 开发的基石。你想想看,如果内存布局都搞错了,后面的所有工作都是白费。我个人建议,每接触一个新平台,先把它的链接脚本从头到尾读一遍,理解每一行的含义,然后再动手写代码。