4、链接脚本与内存布局:链接脚本(.ld文件)编写、代码段/数据段/BSS段分配、Bootloader与App分区规划。

说到链接脚本,很多做MCU开发的朋友可能觉得它很神秘。其实说白了,它就是告诉编译器:你的代码该放哪儿,数据该放哪儿,堆栈又该放哪儿。我刚开始做Bootloader那会儿,也在这上面栽过跟头。有一次芯片死活跑不起来,查了两天才发现是分区地址写错了。嗯,从那以后,我对链接脚本就格外上心了。

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

你想想看,我们写C代码的时候,函数、全局变量、局部变量,它们最终都要变成二进制数据存到Flash里。但问题是,这些数据不能乱放啊。代码段(.text)要放在Flash里,初始化了的全局变量(.data)既要放在Flash里(初始值),又要搬到RAM里(运行时),没初始化的全局变量(.bss)直接占RAM空间就行。

链接脚本就是干这个分配活的。它告诉链接器:

  • Flash从哪个地址开始?多大?
  • RAM从哪个地址开始?多大?
  • 代码段放Flash的哪个位置?
  • 数据段怎么从Flash搬到RAM?
  • 堆栈放在RAM的哪个角落?

我个人习惯,拿到一个新芯片,第一件事就是看它的内存映射手册,然后自己写链接脚本。别看厂家提供的模板能用,但真要搞Bootloader分区,还是自己写的心里有底。

4.2 一个典型的链接脚本长什么样?

以STM32F103为例,我给大家拆解一个实际项目里用的链接脚本。这个芯片Flash是512KB,RAM是64KB。

/* 入口点 */
ENTRY(Reset_Handler)

/* 内存区域定义 */
MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 512K
  RAM   (xrw) : ORIGIN = 0x20000000, LENGTH = 64K
}

/* 段定义 */
SECTIONS
{
  /* 中断向量表,必须放在Flash起始位置 */
  .isr_vector :
  {
    . = ALIGN(4);
    KEEP(*(.isr_vector))
    . = ALIGN(4);
  } > FLASH

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

  /* 只读数据段 */
  .rodata :
  {
    . = ALIGN(4);
    *(.rodata)
    *(.rodata*)
    . = ALIGN(4);
  } > FLASH

  /* 初始化数据段(运行时在RAM,初始值在Flash) */
  _sidata = .;
  .data : AT(_sidata)
  {
    . = ALIGN(4);
    _sdata = .;
    *(.data)
    *(.data*)
    . = ALIGN(4);
    _edata = .;
  } > RAM

  /* BSS段(未初始化数据) */
  .bss :
  {
    . = ALIGN(4);
    _sbss = .;
    __bss_start__ = _sbss;
    *(.bss)
    *(.bss*)
    *(COMMON)
    . = ALIGN(4);
    _ebss = .;
    __bss_end__ = _ebss;
  } > RAM

  /* 堆 */
  ._heap :
  {
    . = ALIGN(8);
    __heap_start = .;
    . = . + _Min_Heap_Size;
    __heap_end = .;
  } > RAM

  /* 栈 */
  ._stack :
  {
    . = ALIGN(8);
    __stack_start = .;
    . = . + _Min_Stack_Size;
    __stack_end = .;
  } > RAM
}

这里有几个关键点我要强调一下:

  • 中断向量表必须放在Flash的起始位置,这是芯片复位后第一个去的地方。
  • _sidata这个符号标记了.data段初始值在Flash里的位置,启动代码要用它来把数据搬到RAM。
  • _sdata和_edata标记了.data段在RAM里的起止位置。
  • _sbss和_ebss标记了.bss段在RAM里的起止位置,启动代码要把这段内存清零。
我的小技巧: 我习惯在链接脚本里多定义几个符号,比如`_flash_start`、`_flash_end`、`_ram_start`、`_ram_end`。这样在C代码里可以直接引用这些地址,做内存检查或者分区计算时特别方便。

4.3 Bootloader与App的分区规划

这才是咱们课程的重头戏。Bootloader和App要共存,就得把Flash分成两个区。我见过很多新手直接把App放在Bootloader后面,结果Bootloader升级了一次,App地址就乱了。

我的分区原则是这样的:

分区 起始地址 大小 内容
Bootloader区 0x08000000 32KB Bootloader代码、中断向量表
参数区 0x08008000 4KB 升级标志、版本号、校验值
App区 0x08009000 476KB 应用程序代码、中断向量表

为什么Bootloader只给32KB?因为Bootloader的功能相对固定,不需要太大。我做过最小的Bootloader只有8KB,就实现了串口升级和CRC校验。参数区4KB用来存一些配置信息,比如升级标志、当前版本号、固件校验值等。

注意: App区的中断向量表不能放在0x08000000了,要偏移到App区的起始地址。这个偏移量需要在App的链接脚本里设置,同时还要在系统初始化时重新设置向量表偏移寄存器。我曾经见过有人忘了这步,结果App里一发生中断就跑飞了。

4.4 App的链接脚本怎么写?

App的链接脚本和Bootloader的差不多,但有几个关键地方要改:

/* App的链接脚本片段 */
MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08009000, LENGTH = 476K
  RAM   (xrw) : ORIGIN = 0x20000000, LENGTH = 64K
}

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

  /* 其他段定义和Bootloader类似,但起始地址变了 */
  /* ... */
}

看到没?Flash的起始地址改成了0x08009000,长度也变成了476KB。这样链接器就会把App的代码从0x08009000开始放。

但光改链接脚本还不够。在App的启动代码里,还要加上这么一句:

/* 设置中断向量表偏移 */
SCB->VTOR = 0x08009000;

这句话告诉CPU:中断向量表不在0x08000000了,挪到0x08009000去了。如果不设这个,CPU一发生中断,还是会跑到Bootloader的向量表里去。

4.5 避坑指南

我在项目中遇到过几个典型问题,分享给大家:

  • 分区大小没对齐: 有些芯片要求分区起始地址必须是某个大小的整数倍(比如1KB或4KB)。我遇到过一个人,他把App区放在0x08008001,结果芯片死活不认。后来查手册才发现,地址必须4字节对齐。
  • RAM分区冲突: Bootloader和App如果共用RAM,一定要规划好。我习惯把RAM分成两部分,Bootloader只用低地址部分,App用高地址部分。这样跳转时不用清RAM,省事。
  • 忘了更新链接脚本: 有一次我改了Flash分区大小,但忘了更新App的链接脚本。结果App编译出来还是按老地址放的,一运行就覆盖了Bootloader。嗯,从那以后我每次改分区都要检查两个链接脚本。

核心要点:

  • 链接脚本就是内存布局的蓝图,必须精确到每个字节
  • Bootloader和App的分区要留够余量,别卡得太死
  • App的中断向量表偏移是跳转成功的关键,忘了设就等着跑飞吧
  • 每次改分区,两个链接脚本都要同步更新

好了,链接脚本和内存布局这块就讲这么多。下一章咱们聊聊启动代码的具体实现,看看Bootloader是怎么把控制权交给App的。说实话,那部分才是真正考验功底的地方。