3. 链接脚本深入:.ld 结构、内存布局与段映射规则
链接脚本,说白了就是告诉链接器:你的芯片内存长什么样,代码该放哪,数据该放哪。很多做应用层开发的同事可能几年都不碰一次 .ld 文件,但做 Bootloader 开发,这关你必须过。
我记得刚入行那会儿,接手一个 STM32 的 Bootloader 项目,编译通过,下载也正常,一跑就死。折腾了两天,最后发现是链接脚本里 Flash 起始地址写错了,Bootloader 把自己覆盖了。嗯,从那以后,我每次改 .ld 文件都会反复核对三遍。
3.1 链接脚本的基本结构
一个标准的 .ld 文件,通常由三大部分组成:
- 内存布局定义(MEMORY):描述芯片的物理存储资源
- 段映射规则(SECTIONS):描述如何将编译产物分配到内存中
- 符号定义与赋值:提供链接时需要的全局符号
来看一个最简的示例,这是我个人习惯的模板结构:
/* 内存布局定义 */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
/* 段映射规则 */
SECTIONS
{
.text :
{
*(.text*)
*(.rodata*)
} > FLASH
.data :
{
*(.data*)
} > RAM AT> FLASH
.bss :
{
*(.bss*)
} > RAM
}
你可能会问:为什么 .data 段要写两遍地址?> RAM AT> FLASH 是什么意思?别急,下面我会详细拆解。
3.2 内存布局定义(MEMORY)
MEMORY 块定义了目标芯片的物理内存资源。每个区域需要指定:
| 属性 | 含义 | 常见取值 |
|---|---|---|
| 名称 | 区域标识符,供 SECTIONS 引用 | FLASH, RAM, CCMRAM |
| 属性 | 访问权限 | r(读), w(写), x(执行) |
| ORIGIN | 起始地址 | 0x08000000(STM32 Flash 起始) |
| LENGTH | 区域大小 | 512K, 128K, 64K |
3.3 段映射规则(SECTIONS)
SECTIONS 是链接脚本的核心。它告诉链接器:每个段(.text, .data, .bss)应该放在哪个内存区域,以及它们的排列顺序。
3.3.1 .text 段:代码与只读数据
.text 段包含程序的可执行指令和只读常量。它通常放在 Flash 中,因为 Bootloader 的代码不需要修改。
.text :
{
/* 中断向量表必须放在最前面 */
. = ALIGN(4);
KEEP(*(.isr_vector))
/* 程序代码 */
*(.text*)
/* 只读数据 */
*(.rodata*)
/* 链接时结束地址 */
_etext = .;
} > FLASH
这里有个细节:KEEP() 是告诉链接器,即使这个段没有被显式引用,也要保留。中断向量表就是典型例子——它没有函数调用它,但 CPU 复位后会从这里取地址。
3.3.2 .data 段:已初始化的全局变量
.data 段存放已初始化的全局变量和静态变量。这些变量在运行时位于 RAM 中,但初始值必须存储在 Flash 里。这就是为什么需要 > RAM AT> FLASH:
- VMA(虚拟地址):运行时地址,在 RAM 中
- LMA(加载地址):存储地址,在 Flash 中
.data : ALIGN(4)
{
_sdata = .; /* RAM 中 .data 起始地址 */
*(.data*)
_edata = .; /* RAM 中 .data 结束地址 */
} > RAM AT> FLASH
/* Flash 中 .data 的存储起始地址 */
_sidata = LOADADDR(.data);
Bootloader 启动时,必须把 .data 从 Flash 拷贝到 RAM。这个拷贝操作依赖 _sdata、_edata 和 _sidata 三个符号。我个人习惯在启动文件里这样写:
/* 汇编伪代码 */
ldr r0, =_sidata /* Flash 源地址 */
ldr r1, =_sdata /* RAM 目标地址 */
ldr r2, =_edata /* RAM 结束地址 */
copy_loop:
cmp r1, r2
bhs copy_done
ldr r3, [r0], #4
str r3, [r1], #4
b copy_loop
copy_done:
3.3.3 .bss 段:未初始化的全局变量
.bss 段存放未初始化或初始化为 0 的全局变量。它只占用 RAM 空间,不占用 Flash 空间。启动时只需要将对应 RAM 区域清零即可。
.bss : ALIGN(4)
{
_sbss = .; /* .bss 起始地址 */
*(.bss*)
*(COMMON)
_ebss = .; /* .bss 结束地址 */
} > RAM
3.4 段映射的完整流程
下面这张图展示了从编译产物到最终内存布局的完整映射关系:
3.5 Bootloader 特有的链接脚本考量
做 Bootloader 时,链接脚本有几个特殊之处需要留意:
- 起始地址偏移:Bootloader 通常从 Flash 起始地址开始,但应用程序需要偏移。比如 Bootloader 占 32KB,应用程序的 Flash 起始地址就是 0x08008000。
- 中断向量表重定位:Bootloader 和应用程序各自有独立的中断向量表。链接脚本中必须为两者分别保留向量表空间。
- RAM 分区:Bootloader 和应用程序可能共享 RAM,也可能各自独占一部分。我建议在链接脚本中明确划分,避免互相踩踏。
实战案例: 某项目需要实现双区 Bootloader(A/B 升级)。我在链接脚本中定义了两个 Flash 区域:
MEMORY
{
BOOT_FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 32K
APP_FLASH_A (rx) : ORIGIN = 0x08008000, LENGTH = 240K
APP_FLASH_B (rx) : ORIGIN = 0x08040000, LENGTH = 240K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
然后通过不同的链接脚本配置,编译出分别对应 A 区和 B 区的固件。升级时,Bootloader 根据标志位决定从哪个区启动。
3.6 常见问题与避坑指南
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 程序跑飞,复位后无法启动 | 中断向量表地址不对 | 检查 .isr_vector 是否放在段首,且地址对齐 |
| 全局变量初始值不对 | .data 段未正确拷贝 | 检查 _sidata/_sdata/_edata 符号是否定义正确 |
| 全局变量值随机 | .bss 段未清零 | 检查启动代码中是否执行了 .bss 清零 |
| 链接时报地址溢出 | 段大小超过 MEMORY 定义 | 检查 LENGTH 是否足够,或优化代码大小 |
objdump -h 或 readelf -S 检查一下各段的 VMA 和 LMA 是否符合预期。
嗯,链接脚本这部分内容确实比较枯燥,但它是 Bootloader 开发的基石。你想想看,如果内存布局都搞错了,后面的所有工作都是白费。我个人建议,每接触一个新平台,先把它的链接脚本从头到尾读一遍,理解每一行的含义,然后再动手写代码。