3、链接脚本详解:链接脚本的作用、MEMORY命令与REGION定义、SECTIONS命令与段布局

好,咱们进入第三个话题。链接脚本,这玩意儿在Bootloader开发里,可以说是「幕后总指挥」。你写的C代码、汇编代码,最终怎么排布到Flash和RAM里,全听它的。

我刚开始做嵌入式那会儿,总觉得链接脚本是编译器自动搞定的,没必要深究。直到有一次,Bootloader跳转后死活跑不起来,查了三天,最后发现是中断向量表放错了位置。嗯,从那以后,我再也不敢小看这个.lds文件了。

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

说白了,链接脚本就是告诉链接器:你的代码段放哪里,数据段放哪里,栈放哪里

你想想看,我们写嵌入式程序,不像PC上那样有操作系统帮忙管理内存。芯片就那么点Flash和RAM,地址都是固定的。链接脚本就是一张「地图」,指引链接器把各个目标文件里的段,安排到具体的物理地址上。

我个人习惯,把链接脚本比作「搬家公司的调度单」——每个家具(段)该放哪个房间(内存区域),调度单上写得清清楚楚。

核心作用总结:

  • 定义目标芯片的内存布局(Flash、RAM的起始地址和大小)
  • 指定各个段的存放位置(.text放Flash,.data放RAM,.bss放RAM)
  • 提供符号地址(比如 _estack、_sidata 这些,供启动代码使用)

3.2 MEMORY命令与REGION定义

先看MEMORY命令。这个命令用来描述目标芯片的物理内存资源

我记得第一次看STM32的链接脚本,里面就写着:

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

这里定义了两个内存区域:

  • FLASH:起始地址0x08000000,大小512KB,属性是rx(可读、可执行)
  • RAM:起始地址0x20000000,大小128KB,属性是xrw(可读、可写、可执行)

属性这块要注意:

属性 含义
r 可读
w 可写
x 可执行
! 取反,比如!x表示不可执行

我曾经踩过的坑: 有一次做Bootloader,把RAM的起始地址写错了,少写了两个0。结果程序一运行就访问到非法地址,直接HardFault。后来我养成了一个习惯——每次拿到新芯片,第一件事就是对着数据手册,把MEMORY区域的地址和大小核对三遍。

如果你做的是Bootloader + 应用程序的双区架构,通常会这样定义:

MEMORY
{
    BOOT_FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 32K
    APP_FLASH  (rx)  : ORIGIN = 0x08008000, LENGTH = 480K
    RAM        (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
}

这样就把Flash分成了两个区域:前32KB给Bootloader,后面的480KB给应用程序。为什么要这么分?说白了就是为了方便做OTA升级——升级应用程序时,Bootloader区域不动,即使升级失败,设备也不会变砖。

3.3 SECTIONS命令与段布局

MEMORY定义好了「房间」,SECTIONS就是安排「家具」怎么放了。

一个典型的SECTIONS布局长这样:

SECTIONS
{
    /* 中断向量表,必须放在Flash起始地址 */
    .isr_vector :
    {
        . = ALIGN(4);
        KEEP(*(.isr_vector))
        . = ALIGN(4);
    } > BOOT_FLASH

    /* 代码段 */
    .text :
    {
        . = ALIGN(4);
        *(.text)
        *(.text*)
        *(.glue_7)
        *(.glue_7t)
        *(.rodata)
        *(.rodata*)
        . = ALIGN(4);
        _etext = .;
    } > BOOT_FLASH

    /* 初始化数据段,运行时从Flash复制到RAM */
    .data : AT (ADDR(.text) + SIZEOF(.text))
    {
        . = ALIGN(4);
        _sdata = .;
        *(.data)
        *(.data*)
        . = ALIGN(4);
        _edata = .;
    } > RAM

    /* BSS段,运行时清零 */
    .bss :
    {
        . = ALIGN(4);
        _sbss = .;
        *(.bss)
        *(.bss*)
        *(COMMON)
        . = ALIGN(4);
        _ebss = .;
    } > RAM
}

这里有几个关键点,我一个个说:

3.3.1 段的位置指定

你看每个段后面都有个 > BOOT_FLASH> RAM,这就是告诉链接器:这个段要放在哪个内存区域里。

.isr_vector.text 放在Flash里,因为它们是只读的,掉电不丢失。.data.bss 放在RAM里,因为运行时需要读写。

3.3.2 加载地址与运行地址

注意 .data 段这里有个 AT 语法:

.data : AT (ADDR(.text) + SIZEOF(.text))

这表示:.data运行时在RAM里,但加载时(也就是烧录时)放在Flash里,紧跟在 .text 段后面。

为什么会这样?因为芯片上电时RAM里的内容是随机的,所以初始化数据必须预先存在Flash里,启动时由Bootloader代码复制到RAM中。这个复制操作,就是靠 _sdata_edata 这些符号来定位的。

我的小技巧: 调试时如果发现全局变量初始值不对,十有八九是链接脚本里 .data 段的加载地址和运行地址没配对。我会在启动代码里加个断点,检查 _sdata_sidata(Flash里的源地址)的值对不对。

3.3.3 对齐与符号

代码里频繁出现的 . = ALIGN(4),是让当前地址按4字节对齐。ARM Cortex-M要求中断向量表必须4字节对齐,否则会出问题。

还有那些 _etext_sdata_ebss 等符号,它们不是变量,而是地址标签。启动代码里会用到它们:

  • _sdata:RAM中.data段的起始地址
  • _edata:RAM中.data段的结束地址
  • _sbss:RAM中.bss段的起始地址
  • _ebss:RAM中.bss段的结束地址

启动代码里大概是这样用的:

/* 复制.data段从Flash到RAM */
ldr r0, =_sidata
ldr r1, =_sdata
ldr r2, =_edata
bl  copy_data

/* 清零.bss段 */
ldr r0, =_sbss
ldr r1, =_ebss
bl  zero_bss

3.4 实战中的链接脚本调整

做Bootloader时,链接脚本经常需要调整。我遇到过几种典型场景:

  • 中断向量表重定位:应用程序的向量表不能放在0x08000000了,要偏移到0x08008000。这时需要在应用程序的链接脚本里,把 .isr_vector 放到 APP_FLASH 区域,并且在代码里设置 SCB->VTOR = 0x08008000
  • 添加自定义段:比如我想在Flash里存一份固件版本信息,可以定义一个 .version_info 段,放在固定地址,方便Bootloader读取。
  • 栈顶地址_estack 通常定义在RAM的末尾,比如 _estack = ORIGIN(RAM) + LENGTH(RAM);。这个值会被写入中断向量表的第一个位置,作为复位后的栈指针。

一句话总结: 链接脚本就是嵌入式程序的「地基」。地基没打好,上面盖的楼再漂亮也得塌。花半小时把链接脚本吃透,能省下后面几天的调试时间。

下一节,咱们就基于这个链接脚本,开始写真正的Bootloader启动代码。到时候你会发现,理解了链接脚本,启动代码里的那些地址操作就一目了然了。