3. Bootloader核心概念
链接脚本,说白了就是告诉链接器:你的代码该往哪里放。很多做应用层开发的同事第一次看到 .lds 文件,会觉得这玩意儿像天书。但搞 Bootloader,这关必须过。
我刚开始做嵌入式时,也跳过链接脚本,直接用默认配置。结果有一次芯片上电就跑飞了,查了三天,最后发现是中断向量表没对齐到正确地址。嗯,从那以后,我再也不敢不读 .lds 了。
3.1 链接脚本的核心语法
一个典型的 .lds 文件,其实就三部分:内存区域定义、段布局、符号导出。我们拆开看。
3.1.1 MEMORY 命令
先告诉链接器,芯片上有哪些可用的内存区域。比如:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
这里定义了 Flash 和 RAM 的起始地址和大小。属性 rx 表示可读可执行,rwx 表示可读可写可执行。我个人习惯把 Bootloader 的 Flash 区域单独划一块,避免和应用程序冲突。
3.1.2 SECTIONS 命令
这是链接脚本的核心。它定义了每个段放在哪个内存区域,以及段内符号的顺序。
SECTIONS
{
.text :
{
*(.isr_vector) /* 中断向量表 */
*(.text) /* 代码段 */
*(.rodata) /* 只读数据 */
_etext = .; /* 代码段结束地址 */
} > FLASH
.data : AT(ADDR(.text) + SIZEOF(.text))
{
_sdata = .; /* 数据段起始地址 */
*(.data) /* 初始化数据 */
_edata = .; /* 数据段结束地址 */
} > RAM
.bss :
{
_sbss = .; /* BSS 段起始地址 */
*(.bss) /* 未初始化数据 */
_ebss = .; /* BSS 段结束地址 */
} > RAM
}
这里有个关键点:.data 段用了 AT 指令。意思是这个段运行时在 RAM,但它的初始值存储在 Flash 里。启动代码必须把这段数据从 Flash 拷贝到 RAM。这个拷贝操作,就是 Bootloader 的必修课。
3.2 代码段、数据段、BSS 段的布局
搞清楚了语法,我们看看实际项目中这些段是怎么排布的。我画了一张图,帮你理清关系。
3.3 栈与堆的配置
栈和堆的配置,是 Bootloader 稳定性的关键。我见过太多因为栈溢出导致的神秘重启,查到最后发现是栈大小设得太抠门。
3.3.1 栈的配置
在链接脚本中,栈通常通过符号定义:
_stack_top = ORIGIN(RAM) + LENGTH(RAM); /* 栈顶在 RAM 末尾 */
_stack_size = 4K; /* 栈大小 4KB */
_stack_bottom = _stack_top - _stack_size; /* 栈底 */
启动代码里,复位向量会初始化 SP 寄存器指向 _stack_top。注意,ARM Cortex-M 系列的栈是满递减栈,SP 初始指向栈顶,压栈时地址减小。
3.3.2 堆的配置
堆用于动态内存分配。在 Bootloader 里,我建议尽量少用堆。为什么?因为堆管理有不确定性,碎片化问题在资源受限的系统里很致命。
如果非要用,可以在链接脚本里定义堆的边界:
_heap_start = _ebss + 4; /* BSS 段之后 */
_heap_end = _stack_bottom; /* 栈底之前 */
这样堆和栈之间就有了明确的界限。堆向上增长,栈向下增长,两者在中间相遇时就是内存耗尽。
3.4 链接脚本中的符号导出
链接脚本里定义的符号,可以在 C 代码中直接引用。这是 Bootloader 和应用程序之间传递信息的常用手段。
/* 链接脚本中定义 */
_flash_start = ORIGIN(FLASH);
_flash_size = LENGTH(FLASH);
_app_start = 0x08010000; /* 应用程序起始地址 */
/* C 代码中引用 */
extern uint32_t _flash_start;
extern uint32_t _app_start;
void jump_to_app(void)
{
uint32_t app_addr = (uint32_t)&_app_start;
// 跳转到应用程序
}
这里要注意,链接脚本里定义的符号,在 C 代码中要当作地址来用。取地址操作符 & 拿到的是符号的值,也就是那个地址本身。
3.5 实战中的段布局策略
不同场景下,段布局的策略也不同。我整理了一个表格,方便你对照选择。
| 场景 | Flash 布局 | RAM 布局 | 注意事项 |
|---|---|---|---|
| 单 Bootloader | 向量表 + 代码 + 只读数据 | 数据段 + BSS + 堆 + 栈 | 最简单,适合小容量芯片 |
| Bootloader + 应用程序 | Boot 区 + App 区 + 配置区 | 分时复用,Boot 和 App 共用 RAM | 需要定义跳转协议 |
| 双备份升级 | Boot 区 + App A + App B | 运行时只加载一个 App | Flash 容量要求高,可靠性强 |
| 带参数存储 | 代码区 + 参数区(最后几页) | 正常布局 + 参数缓存 | 参数区要按页对齐 |
3.6 一个完整的 .lds 示例
最后,给你一个我实际项目里用过的链接脚本框架。你可以直接拿来改。
/* bootloader.lds */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 32K
}
_estack = ORIGIN(RAM) + LENGTH(RAM);
SECTIONS
{
.isr_vector :
{
KEEP(*(.isr_vector))
} > FLASH
.text :
{
*(.text*)
*(.rodata*)
_etext = .;
} > FLASH
.data : AT(ADDR(.text) + SIZEOF(.text))
{
_sdata = .;
*(.data*)
_edata = .;
} > RAM
.bss :
{
_sbss = .;
*(.bss*)
*(COMMON)
_ebss = .;
} > RAM
/* 堆定义 */
.heap :
{
_heap_start = .;
. = . + 2K;
_heap_end = .;
} > RAM
/* 栈定义 */
.stack :
{
_stack_start = .;
. = . + 2K;
_stack_end = .;
} > RAM
}
- 链接脚本是内存布局的「宪法」,所有段的位置都由它决定
- .data 段需要从 Flash 拷贝到 RAM,启动代码必须实现这个拷贝
- .bss 段需要在启动时清零,否则未初始化的全局变量会是随机值
- 栈和堆的大小要根据实际需求配置,留足余量
- 链接脚本中定义的符号,C 代码可以通过 extern 引用
链接脚本这东西,初看觉得复杂,用多了就会发现它其实很直观。你只要记住:代码放 Flash,变量放 RAM,栈顶在 RAM 末尾,基本就不会出大问题。至于更高级的段重定位、分散加载,那是后面章节的内容了。