3. 链接脚本与内存布局:.ld 文件、段布局与分区设计
链接脚本,说白了就是告诉链接器:你的代码该放哪,数据该放哪,RAM 和 Flash 怎么分。很多新手觉得这玩意儿枯燥,直接拿厂家例程改改就用了。我刚开始做 Bootloader 时也这么干,结果有一次芯片死活跑不起来,折腾了两天才发现——是链接脚本里 RAM 地址写错了。
嗯,从那以后我再也不敢轻视 .ld 文件了。今天咱们就把这块彻底讲透。
3.1 链接脚本 (.ld) 到底在干什么?
一个嵌入式项目从源码到可执行文件,要经过:预处理 → 编译 → 汇编 → 链接。前几步生成的都是目标文件 (.o),里面是分散的代码段、数据段。链接脚本的任务,就是把这些碎片拼成一个完整的镜像,并指定每个段在芯片物理地址空间中的位置。
核心概念:链接脚本 = 内存地图 + 段排列规则。没有它,你的 .bin 文件就是一盘散沙。
一个典型的 .ld 文件包含三个部分:内存区域定义 (MEMORY)、段定义 (SECTIONS)、符号赋值。咱们逐个拆解。
3.2 MEMORY 命令:划地盘
先告诉链接器芯片上有哪些可用的内存区域,以及它们的起始地址和大小。
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
这里定义了 Flash 和 RAM 两块区域。属性 (rx) 表示可读可执行,(rwx) 表示可读可写可执行。我个人习惯把 Bootloader 的 Flash 单独划一块,比如从 0x08000000 开始只给 64KB,剩下的留给应用程序。这样即使 Bootloader 写崩了,也不影响应用区。
| 属性 | 含义 | 典型用途 |
|---|---|---|
| r | 可读 | 只读数据区 |
| w | 可写 | RAM 数据区 |
| x | 可执行 | 代码区 |
| ! | 取反 | 排除某属性 |
经验之谈:Bootloader 的 Flash 区域建议放在芯片 Flash 的最开头,因为芯片复位后 PC 指针默认指向 0x08000000(或 0x00000000,取决于映射)。我曾经见过有人把 Bootloader 放在 Flash 末尾,结果每次启动都要先跳转,多了一步不说,还容易出问题。
3.3 SECTIONS 命令:排兵布阵
这是链接脚本的核心。它定义了每个段放在哪个内存区域,以及段之间的排列顺序。
SECTIONS
{
.text :
{
*(.isr_vector) /* 中断向量表 */
*(.text) /* 代码 */
*(.rodata) /* 只读数据 */
_etext = .; /* 代码结束地址 */
} > FLASH
.data : AT(_etext)
{
_sdata = .;
*(.data)
_edata = .;
} > RAM
.bss :
{
_sbss = .;
*(.bss)
_ebss = .;
} > RAM
}
这里有几个关键点:
- .text 段:包含中断向量表、代码、常量。放在 Flash 里,掉电不丢失。
- .data 段:已初始化的全局变量。运行时在 RAM 里,但初始值存在 Flash 里。启动时由 Bootloader 从 Flash 拷贝到 RAM。
- .bss 段:未初始化的全局变量。运行时在 RAM 里,启动时清零。
你注意看 .data : AT(_etext) 这一行。AT 指定了加载地址(在 Flash 中的位置),而 > RAM 指定了运行地址。这就是所谓的「加载时在 Flash,运行时在 RAM」。启动代码必须负责把数据从 Flash 拷贝到 RAM,这个拷贝过程通常由汇编写的 Reset_Handler 完成。
踩坑提醒:我曾经遇到一个项目,.data 段拷贝没做,结果全局变量初始值全是乱的。现象很诡异——有些变量值是对的,有些是错的。后来发现是链接脚本里 _etext 符号没正确导出,启动代码拿到的拷贝地址是错的。所以一定要检查启动代码里是否引用了 _sdata、_edata、_etext 这些符号。
3.4 代码段 / 数据段 / BSS 段布局
咱们用一张图来直观理解最终的内存布局:
3.5 RAM 与 Flash 分区设计
做 Bootloader 时,分区设计直接决定了升级方案的可行性。我个人习惯把 Flash 分成三个区:
| 分区 | 起始地址 | 大小 | 内容 |
|---|---|---|---|
| Bootloader 区 | 0x08000000 | 64KB | Bootloader 代码、向量表 |
| 应用区 (App) | 0x08010000 | 384KB | 应用程序代码、向量表 |
| 备份/下载区 | 0x08070000 | 64KB | 升级固件暂存 |
RAM 分区也要规划好:
- Bootloader 专用 RAM:通常 4-8KB,用于 Bootloader 自身的栈和变量。这部分应用不能碰。
- 应用 RAM:剩下的给应用程序。Bootloader 跳转到应用前,要把自己用的 RAM 清理干净,否则应用启动时可能读到脏数据。
- 共享区域:比如升级标志、版本号,放在固定地址,Bootloader 和应用都能访问。
我的做法:在链接脚本里用 PROVIDE 关键字导出一些关键符号,比如 PROVIDE(_bootloader_size = 0x10000);。这样在 C 代码里可以直接引用这些符号,不用硬编码地址。改分区大小只需要改一处,不容易出错。
3.6 实战:一个 Bootloader 的链接脚本示例
下面是我常用的一个 Bootloader 链接脚本模板,加了详细注释:
/* Bootloader 链接脚本 - STM32F103 示例 */
ENTRY(Reset_Handler)
MEMORY
{
/* Bootloader 占用 Flash 前 64KB */
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
/* 应用从 0x08010000 开始,384KB */
APP_FLASH (rx) : ORIGIN = 0x08010000, LENGTH = 384K
/* RAM 全部可用,但 Bootloader 只用前 8KB */
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 8K
}
SECTIONS
{
.text :
{
KEEP(*(.isr_vector))
*(.text*)
*(.rodata*)
. = ALIGN(4);
_etext = .;
} > FLASH
.data : AT(_etext)
{
_sdata = .;
*(.data*)
. = ALIGN(4);
_edata = .;
} > RAM
.bss :
{
_sbss = .;
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
} > RAM
/* 栈顶指针,指向 RAM 末尾 */
_estack = ORIGIN(RAM) + LENGTH(RAM);
}
这里有几个细节要注意:
KEEP(*(.isr_vector)):强制保留中断向量表,即使没被引用也不能被优化掉。. = ALIGN(4);:4 字节对齐,ARM Cortex-M 要求向量表必须对齐到 256 字节边界,但代码段 4 字节对齐就够了。_estack:栈顶指针,指向 RAM 最高地址。ARM 的栈是向下生长的,所以栈顶在 RAM 末尾。
又一个坑:我曾经在升级时发现,新固件下载完了,跳转过去却跑不起来。查了半天,原来是 Bootloader 的链接脚本里把 RAM 全占了(LENGTH = 128K),应用启动时发现自己没 RAM 可用。后来我把 Bootloader 的 RAM 限制在 8KB,剩下的留给应用,问题解决。记住:Bootloader 要「用完即走」,别占着茅坑不拉屎。
3.7 链接脚本调试技巧
链接脚本出了问题,最直接的表现就是程序跑飞或变量值不对。怎么排查?
- 查看 map 文件:编译时加
-Wl,-Map=output.map,生成 map 文件。里面详细记录了每个段、每个符号的地址。我遇到地址冲突时,第一件事就是看 map 文件。 - 检查符号值:在启动代码里加几行调试输出,打印 _sdata、_edata、_etext 的值,看是否和预期一致。
- 用 objdump 反汇编:
arm-none-eabi-objdump -D bootloader.elf,看反汇编代码中的地址是否正确。 - 仿真器单步:在 IDE 里加载 .elf 文件,看反汇编窗口中的地址是否落在正确的 Flash 区域。
嗯,链接脚本这东西,说难不难,说简单也不简单。关键是要理解它背后的逻辑——你是在给芯片画一张内存地图,每个段、每个符号都要有明确的位置。搞懂了这些,后面讲启动流程、中断向量表重映射,你就知道为什么链接脚本要那么写了。