4. 链接脚本与内存布局
链接脚本(Linker Script)是 Bootloader 开发中连接编译器输出与硬件物理内存的桥梁。它决定了代码段、数据段、BSS 段在 Flash 和 RAM 中的最终位置,并直接影响启动流程中栈与堆的初始化。本章从语法入手,逐步深入到布局设计与初始化实现。
4.1 链接脚本(.lds)语法基础
GNU LD 链接器使用 .lds 文件描述输入段的合并规则与输出段的地址映射。一个典型的链接脚本包含以下核心元素:
| 语法元素 | 作用 | 示例 |
|---|---|---|
| ENTRY | 指定程序入口点(第一条指令地址) | ENTRY(Reset_Handler) |
| MEMORY | 定义目标硬件的物理内存区域(名称、起始地址、长度、属性) | FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K |
| SECTIONS | 描述输出段如何从输入段中提取并放置到指定内存区域 | .text : { *(.text) } > FLASH |
| PROVIDE | 在脚本中定义符号,供 C 代码引用(如栈顶地址) | PROVIDE(_estack = ORIGIN(RAM) + LENGTH(RAM)); |
以下是一个面向 ARM Cortex-M 的 Bootloader 最小链接脚本框架:
ENTRY(Reset_Handler)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 32K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 8K
}
SECTIONS
{
.isr_vector :
{
KEEP(*(.isr_vector))
} > FLASH
.text :
{
*(.text*)
*(.rodata*)
} > FLASH
.data : ALIGN(4)
{
_sdata = .;
*(.data*)
_edata = .;
} > RAM AT> FLASH
.bss : ALIGN(4)
{
_sbss = .;
*(.bss*)
*(COMMON)
_ebss = .;
} > RAM
PROVIDE(_estack = ORIGIN(RAM) + LENGTH(RAM));
PROVIDE(_heap_start = _ebss);
PROVIDE(_heap_end = ORIGIN(RAM) + LENGTH(RAM) - 1K);
}
4.2 代码段、数据段与 BSS 段布局
Bootloader 的运行时内存布局必须精确匹配链接脚本的描述。三个关键段各有不同的生命周期和存放位置:
- .text(代码段 + 只读数据):包含可执行指令和常量。存放在 Flash 中,CPU 通过指令总线直接读取。Bootloader 的启动代码、Flash 驱动、通信协议栈均在此段。
- .data(已初始化数据段):包含全局变量和静态变量的初始值。运行时位于 RAM,但初始值存储在 Flash 中。启动代码必须将初始值从 Flash 复制到 RAM 中对应的地址。
- .bss(未初始化数据段):包含全局变量和静态变量(默认值为 0)。运行时位于 RAM,启动代码必须将其清零。
下图展示了 Bootloader 在 Flash 和 RAM 中的典型布局关系:
4.3 栈与堆的初始化设置
栈(Stack)和堆(Heap)是 Bootloader 运行时的两个关键内存区域。栈用于函数调用、局部变量和中断嵌套;堆用于动态内存分配(如通信缓冲区)。它们的初始化必须在进入 main() 之前完成。
4.3.1 栈指针(SP)初始化
ARM Cortex-M 系列处理器在复位后会自动从 Flash 起始地址(0x00000000 或 0x08000000)加载栈指针。链接脚本中通过 PROVIDE(_estack = ...) 定义栈顶地址,并由汇编启动代码写入 MSP(主栈指针):
; 汇编启动代码片段(reset_handler)
ldr r0, =_estack ; 加载栈顶地址
msr msp, r0 ; 写入主栈指针
bl main ; 跳转到 C 主函数
4.3.2 堆初始化与符号导出
堆的起始地址通常紧接在 .bss 段之后。链接脚本中通过 _heap_start = _ebss 和 _heap_end 定义堆边界。C 运行时库(如 newlib)依赖这些符号来管理 malloc/free:
/* 在 C 启动代码中声明外部符号 */
extern uint32_t _estack;
extern uint32_t _heap_start;
extern uint32_t _heap_end;
/* 堆初始化函数(供 _sbrk 使用) */
caddr_t _sbrk(int incr)
{
static uint32_t *heap_ptr = NULL;
uint32_t *prev_heap_ptr;
if (heap_ptr == NULL)
heap_ptr = (uint32_t *)&_heap_start;
prev_heap_ptr = heap_ptr;
heap_ptr += incr;
if (heap_ptr > (uint32_t *)&_heap_end)
return (caddr_t)-1; /* 堆溢出 */
return (caddr_t)prev_heap_ptr;
}
4.3.3 栈与堆的冲突避免
栈向下增长,堆向上增长。两者在 RAM 中相向而行,必须预留安全间隙。下表总结了典型配置策略:
| 内存区域 | 起始地址 | 大小 | 增长方向 |
|---|---|---|---|
| .data | RAM 起始(0x20000000) | 由链接脚本计算 | — |
| .bss | .data 结束 | 由链接脚本计算 | — |
| 堆 | .bss 结束(_heap_start) | 通常 1KB ~ 4KB | 向上(高地址) |
| 栈 | RAM 末尾(_estack) | 通常 1KB ~ 2KB | 向下(低地址) |
下图展示了栈与堆在 RAM 中的相向增长关系及安全保护区的位置:
4.4 启动代码中的段初始化流程
Bootloader 的启动代码(Reset_Handler)必须按以下顺序完成内存初始化,才能安全进入 C 环境:
- 加载栈指针:从链接符号
_estack加载并写入 MSP。 - 复制 .data 段:从 Flash(LMA)复制到 RAM(VMA),长度由
_edata - _sdata计算。 - 清零 .bss 段:将
_sbss到_ebss之间的内存全部写 0。 - 初始化堆:设置
_sbrk的堆基址(如果使用动态内存)。 - 调用 main():此时全局变量已就绪,栈指针正确,可以执行 C 代码。
_sdata、_edata、_sbss、_ebss 四个符号的值是否与链接脚本中的地址一致。任何偏差都会导致全局变量初始化失败。