第4章:链接脚本与内存映射

链接脚本,说白了就是告诉编译器:你的代码该放哪儿,数据该放哪儿。很多刚入行的朋友觉得这玩意儿枯燥,直接拿现成的改改就用了。但我跟你说,Bootloader能不能跑起来,一半的功夫就在这脚本里。

我个人习惯,每做一个新平台,第一件事就是先把内存映射图画出来。别急着写代码,画图花不了半小时,但能省下后面几天的调试时间。

4.1 链接脚本(.icf/.ld)到底在干什么

链接脚本的本质,就是一张地址分配表。它告诉链接器:

  • 你的Flash从哪个地址开始
  • RAM又在哪里
  • 哪些函数要放在特定位置
  • 中断向量表该放哪

IAR用.icf文件,GCC用.ld文件。语法不同,但核心思想一样。我当年从IAR转到GCC时,也花了点时间适应。其实你理解了内存模型,换工具链就是换个写法的事。

核心要点:链接脚本定义了程序的物理布局。Bootloader和应用程序共用同一颗芯片,必须通过链接脚本把它们的地址空间彻底隔开。

4.2 代码段/数据段/BSS段布局

一个典型的嵌入式程序,内存里分这么几块:

段名 存放内容 存放位置 特点
.text 代码、常量、字符串 Flash 只读,掉电不丢失
.rodata 只读数据 Flash 常量、查表数据
.data 已初始化全局变量 Flash(初始值) + RAM(运行时) 启动时从Flash拷贝到RAM
.bss 未初始化全局变量 RAM 启动时清零
.stack 栈空间 RAM 函数调用、局部变量
.heap 堆空间 RAM 动态分配(malloc)

这里有个坑,我必须要说。.data段在Flash里存了一份初始值,启动时由启动代码拷贝到RAM。如果你Bootloader里用了很多全局变量,这个拷贝过程会占用启动时间。我曾经在一个项目里,Bootloader启动慢了200ms,就是因为.data段太大。后来我把大部分全局变量改成了局部变量,问题就解决了。

我的建议:Bootloader里尽量少用全局变量。能用局部变量就用局部变量。实在要用,考虑用const修饰,直接放在Flash里读取。

4.3 中断向量表重映射

这是Bootloader里最容易出问题的地方。芯片上电后,默认从Flash起始地址(通常是0x00000000或0x08000000)取中断向量表。但Bootloader和应用程序各有一套中断向量表,怎么办?

答案就是:重映射。

以STM32为例,Bootloader放在Flash开头,应用程序放在后面某个偏移地址。当Bootloader跳转到应用程序前,必须做两件事:

  1. 修改VTOR寄存器,指向应用程序的中断向量表地址
  2. 设置主堆栈指针(MSP)为应用程序的栈顶

代码大概长这样:

/* 假设应用程序起始地址为0x08020000 */
#define APP_ADDRESS     0x08020000
#define APP_STACK_ADDR  (*(volatile uint32_t*)APP_ADDRESS)
#define APP_RESET_HANDLER (*(volatile uint32_t*)(APP_ADDRESS + 4))

void jump_to_app(void)
{
    /* 关闭全局中断 */
    __disable_irq();
    
    /* 重映射中断向量表 */
    SCB->VTOR = APP_ADDRESS;
    
    /* 设置主栈指针 */
    __set_MSP(APP_STACK_ADDR);
    
    /* 跳转到应用程序复位向量 */
    void (*app_reset)(void) = (void (*)(void))APP_RESET_HANDLER;
    app_reset();
}

嗯,这里要注意。跳转前一定要关中断。为什么?因为跳转过程中,如果来了个中断,PC指针还在Bootloader里,但中断向量表已经指向应用程序了,这就会导致程序跑飞。我刚开始做Bootloader时,就因为这个原因,调试了整整两天。

避坑指南:我曾经在一个项目里,跳转后外设中断一直触发。查了半天,发现是跳转前没有把外设时钟关掉。应用程序初始化时,外设状态不确定,导致异常。所以我的习惯是:跳转前把所有用过的外设都复位到默认状态。

4.4 链接脚本实战示例

下面给一个IAR的.icf文件示例,注释里我写了关键点:

/* Bootloader链接脚本 - 基于STM32F407 */
/* Flash: 0x08000000 - 0x0801FFFF (128KB) */
/* RAM:   0x20000000 - 0x2001FFFF (128KB) */

define symbol __ICFEDIT_region_ROM_start__  = 0x08000000;
define symbol __ICFEDIT_region_ROM_end__    = 0x0801FFFF;
define symbol __ICFEDIT_region_RAM_start__  = 0x20000000;
define symbol __ICFEDIT_region_RAM_end__    = 0x2001FFFF;

/* 中断向量表放在Flash开头 */
place at address mem:0x08000000 { readonly section .intvec };

/* 代码段紧随其后 */
place in ROM_region { readonly };

/* 数据段放在RAM */
place in RAM_region { readwrite,
                      block CSTACK,
                      block HEAP };

你看,这个脚本把Bootloader限制在了128KB的Flash空间里。应用程序的链接脚本就要从0x08020000开始了。两个脚本配合好,各管各的地盘,互不干扰。

4.5 内存映射的检查方法

写完链接脚本,怎么确认对不对?我的做法是:

  • 编译后看.map文件,检查各段的起始地址和大小
  • 用调试器读VTOR寄存器,确认中断向量表地址
  • 在跳转前打印关键地址,验证数据一致性

我记得有一次,同事的Bootloader死活跳不过去。我让他把.map文件发给我一看,发现.data段的LMA(加载地址)和VMA(运行地址)搞反了。这种错误,光看代码是看不出来的,必须看.map文件。

总结一下:链接脚本和内存映射是Bootloader的骨架。骨架搭歪了,后面再怎么填肉都没用。花时间把这块吃透,后面写代码会顺畅很多。