3. 链接脚本详解:链接脚本(.ld)的作用、内存布局定义、段(text/data/bss)的分配规则

做 Bootloader 开发,绕不开链接脚本。很多人觉得它神秘,其实说白了,它就是告诉链接器:你的代码该放哪,数据该放哪,堆栈又该放哪。

我刚开始做嵌入式时,也跳过这步。结果呢?程序跑飞了,查了两天才发现是 .bss 段没清零。从那以后,我养成了一个习惯——写任何固件前,先把 .ld 文件理清楚。

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

你写好的 C 代码,经过编译变成目标文件(.o)。这些 .o 文件里,代码、数据、未初始化变量都混在一起。链接脚本的作用,就是把它们按你指定的规则,安排到目标芯片的存储空间里。

举个例子:

// 编译后的段分布
.text   → 代码
.rodata → 只读数据(字符串、常量)
.data   → 已初始化全局变量
.bss    → 未初始化全局变量

链接脚本就是一张“地图”,告诉链接器每个段该去哪个地址安家。

核心要点:链接脚本 = 内存布局的“宪法”。所有段都必须遵守它定义的地址规则。

3.2 内存布局定义——你得先知道芯片有啥

写链接脚本前,你得搞清楚目标芯片的内存分布。以 STM32F103 为例:

存储区域 起始地址 大小 用途
Flash 0x08000000 512KB 存放代码和只读数据
SRAM 0x20000000 64KB 存放变量、堆栈
System Memory 0x1FFF0000 — 系统 Bootloader

这些地址,就是链接脚本的“地基”。你想想看,如果 Flash 地址写错了,代码烧进去根本跑不起来。

我曾经踩过的坑:有一次把 Flash 起始地址写成了 0x08001000,结果中断向量表偏移了,芯片一上电就进 HardFault。查了整整半天。

3.3 段分配规则——.text / .data / .bss 怎么放?

这是链接脚本的核心。我习惯用一个简单的模板来说明:

MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 512K
    SRAM  (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
}

SECTIONS
{
    .text :
    {
        *(.isr_vector)    /* 中断向量表 */
        *(.text)          /* 代码 */
        *(.rodata)        /* 只读数据 */
        _etext = .;       /* 代码结束地址 */
    } > FLASH

    .data : AT(_etext)
    {
        _sdata = .;
        *(.data)
        _edata = .;
    } > SRAM

    .bss :
    {
        _sbss = .;
        *(.bss)
        _ebss = .;
    } > SRAM
}

这里有几个关键点:

  • .text 段放 Flash:代码和常量,掉电不丢失。
  • .data 段放 SRAM,但加载地址在 Flash:启动时从 Flash 拷贝到 SRAM。
  • .bss 段放 SRAM:启动时由启动代码清零。

个人经验:我习惯在 .data 段前后定义 _sdata 和 _edata 符号,这样启动代码可以直接用这些符号做拷贝。别用硬编码地址,否则换个芯片就得改。

3.4 为什么 .data 要搞两套地址?

这个问题我经常被问到。你看上面的脚本,.data 段有两个地址:

  • VMA(虚拟地址):运行时在 SRAM 中的地址。
  • LMA(加载地址):烧录时在 Flash 中的地址。

为什么会这样?因为全局变量初始值必须存到 Flash 里(掉电不丢),但运行时必须放到 SRAM 里(才能读写)。所以启动时,Bootloader 要把 .data 从 Flash 拷贝到 SRAM。

我见过有人忘了做这个拷贝,结果全局变量全是 0。嗯,这种 bug 最难查。

3.5 .bss 段——为什么必须清零?

.bss 段放的是未初始化的全局变量。C 标准规定,这些变量默认值为 0。但硬件上电后,SRAM 里的值是随机的。所以启动代码必须把 .bss 段全部清零。

看这段启动代码:

extern uint32_t _sbss, _ebss;

void _start(void)
{
    uint32_t *p = &_sbss;
    while (p < &_ebss) {
        *p++ = 0;
    }
    /* 然后跳转到 main */
}

我曾经犯过的错:有一次为了省启动时间,跳过了 .bss 清零。结果一个全局标志位随机为 1,导致程序逻辑错乱。从那以后,我再也不敢省这一步。

3.6 链接脚本中的符号——链接器帮你算地址

链接脚本里可以定义符号,这些符号在 C 代码里可以直接用 extern 引用。常用的有:

符号 含义 典型用途
_etext 代码段结束地址 .data 拷贝的源地址
_sdata / _edata .data 段的起止地址 拷贝循环的边界
_sbss / _ebss .bss 段的起止地址 清零循环的边界
_estack 栈顶地址 中断向量表初始值

这些符号是链接器在链接阶段计算出来的,不是程序员手动定义的。你只需要在链接脚本里声明,C 代码里引用就行。

3.7 链接脚本的常见陷阱

做 Bootloader 时,有几个地方特别容易出问题:

  • 中断向量表偏移:如果 Bootloader 和 App 共用 Flash,App 的向量表必须偏移。链接脚本里要定义 VECT_TAB_OFFSET。
  • 段对齐:ARM 要求代码段 4 字节对齐,否则可能触发异常。
  • 栈和堆的位置:我习惯把栈放在 SRAM 末尾,堆紧随 .bss 之后。这样栈向下增长,堆向上增长,不容易溢出。

避坑指南:写链接脚本时,一定要用 MEMORY 命令定义好每个区域的起止地址。别偷懒用默认值,否则芯片换了,脚本也得重写。

3.8 本章知识结构图

下面这张图总结了链接脚本的核心逻辑:

链接脚本核心结构 目标文件 (.o) 链接脚本 (.ld) 芯片内存布局 链接器 (ld) 可执行文件 (.elf) .text → Flash 代码 + 只读数据 .data → SRAM 已初始化全局变量 .bss → SRAM 未初始化全局变量 启动时:.data 从 Flash 拷贝到 SRAM,.bss 清零 链接脚本中的符号(_sdata、_ebss 等)供启动代码使用

3.9 小结

链接脚本是 Bootloader 开发的基石。你不需要背下所有语法,但一定要理解:

  • MEMORY 定义芯片的物理存储区域。
  • SECTIONS 定义每个段放哪,以及加载地址和运行地址的关系。
  • 符号是链接器帮你算的,别自己硬编码地址。

我个人习惯,每写一个新 Bootloader,第一件事就是打开 .ld 文件,确认内存布局对不对。这一步省了,后面全是坑。