第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跳转到应用程序前,必须做两件事:
- 修改VTOR寄存器,指向应用程序的中断向量表地址
- 设置主堆栈指针(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的骨架。骨架搭歪了,后面再怎么填肉都没用。花时间把这块吃透,后面写代码会顺畅很多。