4. 链接脚本与内存布局
链接脚本,说白了就是告诉链接器:你的代码该放哪儿,数据该放哪儿。很多做嵌入式开发的朋友,写了好几年代码,都没碰过 .lds 文件。但做 Bootloader 开发,这关你绕不过去。
为什么?因为 Bootloader 跑在裸机上,没有操作系统帮你管理内存。你得自己告诉编译器,哪块地址是 ROM,哪块是 RAM,中断向量表放哪个位置。搞错了,芯片直接跑飞。
我个人习惯,每做一个新平台,第一件事就是先把链接脚本搭好。这一步稳了,后面才踏实。
4.1 链接脚本语法基础
GNU LD 的链接脚本,扩展名通常是 .lds。语法看着有点怪,但核心结构其实不复杂。
OUTPUT_FORMAT("elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
. = 0x00000000;
.text : { *(.text) }
.data : { *(.data) }
.bss : { *(.bss) }
}
上面这个是最简版本。咱们拆开看:
- OUTPUT_FORMAT:指定输出文件的格式。ARM 平台常用
elf32-littlearm。 - OUTPUT_ARCH:目标架构。告诉链接器这是给 ARM 用的。
- ENTRY:入口点。程序第一条指令的标号,通常是
_start。 - SECTIONS:核心部分。定义各个段怎么放。
这里有个关键符号——.(点号)。它叫位置计数器,代表当前输出地址。你给 . 赋个值,就等于说「从这儿开始放」。我在项目中见过有人忘了设置 . 的初始值,结果链接出来的镜像地址全乱套了。
ld -M 或 objdump -h 查看最终的内存映射。这比盯着代码猜靠谱多了。
4.2 内存区域定义
实际项目中,芯片的内存空间不是连续的。Flash 在 0x08000000,SRAM 在 0x20000000。你得用 MEMORY 命令把这些区域告诉链接器。
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
每个区域有三个属性:
| 属性 | 含义 | 说明 |
|---|---|---|
| r | 可读 | 几乎所有内存区域都有 |
| w | 可写 | RAM 必须有,Flash 一般没有 |
| x | 可执行 | 代码段所在区域必须有 |
定义好 MEMORY 之后,在 SECTIONS 里就可以用 >FLASH 这样的语法,把段分配到指定区域。
ORIGIN 和 LENGTH 必须跟芯片手册一致。我曾经把 SRAM 的起始地址写错了 0x100,结果程序一跑就死,查了整整两天。
4.3 段的放置
一个典型的嵌入式程序,至少包含三个段:.text、.data、.bss。咱们一个一个说。
.text 段:代码和只读数据
这个段放的是程序指令和常量。它必须放在非易失性存储器里,比如 Flash。上电就能直接执行。
.text :
{
*(.text)
*(.text.*)
*(.rodata)
*(.rodata.*)
} >FLASH
注意我加了 .text.* 和 .rodata.*。这是通配符,用来捕获编译器生成的子段。不加的话,有些段会漏掉。
.data 段:已初始化的全局变量
这个段有点特殊。变量有初始值,但值存在 Flash 里,运行时却要搬到 RAM 里才能读写。所以 .data 在链接脚本里要出现两次:
.data : AT (ADDR(.text) + SIZEOF(.text))
{
_sdata = .;
*(.data)
*(.data.*)
_edata = .;
} >SRAM
这里 AT 指定了加载地址(在 Flash 里),而 >SRAM 指定了运行地址(在 RAM 里)。启动代码里需要把数据从 Flash 拷贝到 RAM,这个拷贝的起止地址就是 _sdata 和 _edata。
_sdata 和 _edata 是链接器生成的符号,启动代码里可以直接引用。这是 Bootloader 初始化过程中必不可少的一步。
.bss 段:未初始化的全局变量
.bss 段不占 Flash 空间,只占 RAM 空间。启动时要把这段内存清零。
.bss :
{
_sbss = .;
*(.bss)
*(.bss.*)
*(COMMON)
_ebss = .;
} >SRAM
COMMON 是编译器生成的未初始化全局变量,也要归到 .bss 里。
4.4 栈与堆的配置
栈和堆在链接脚本里没有专门的段,但它们占用的内存你得预留出来。我一般这样处理:
_estack = ORIGIN(SRAM) + LENGTH(SRAM);
_heap_size = 4K;
_stack_size = 8K;
然后在 .bss 后面加上:
_heap_start = _ebss;
_heap_end = _heap_start + _heap_size;
_stack_start = _heap_end;
_stack_end = _stack_start + _stack_size;
这样,RAM 的布局就是:.data → .bss → 堆 → 栈。栈顶指向 RAM 的最高地址,向下生长。
4.5 完整示例与内存布局图
下面是一个完整的 STM32 Bootloader 链接脚本:
OUTPUT_FORMAT("elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(Reset_Handler)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 32K
SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}
_estack = ORIGIN(SRAM) + LENGTH(SRAM);
SECTIONS
{
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} >FLASH
.text :
{
. = ALIGN(4);
*(.text)
*(.text.*)
*(.rodata)
*(.rodata.*)
. = ALIGN(4);
_etext = .;
} >FLASH
.data : AT (_etext)
{
. = ALIGN(4);
_sdata = .;
*(.data)
*(.data.*)
. = ALIGN(4);
_edata = .;
} >SRAM
.bss :
{
. = ALIGN(4);
_sbss = .;
*(.bss)
*(.bss.*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
} >SRAM
. = ALIGN(4);
_heap_start = .;
_heap_end = _heap_start + 4K;
_stack_start = _heap_end;
_stack_end = _stack_start + 8K;
}
对应的内存布局,我画了张图,方便你理解:
.data 段在 Flash 和 SRAM 里各有一份。Flash 里是初始值,SRAM 里是运行时的副本。启动代码必须完成这个拷贝,否则全局变量全是错的。
4.6 常见坑与避坑指南
- 对齐问题:ARM 要求代码 4 字节对齐。忘了加
. = ALIGN(4),可能触发硬件异常。 - 段顺序:中断向量表必须放在最前面,地址 0 或 0x08000000。放错了,芯片上电直接跑飞。
- 栈顶地址:
_estack必须是 RAM 的最高地址。我见过有人写成ORIGIN(SRAM) + LENGTH(SRAM) - 1,结果栈顶偏了一个字节,虽然能用但心里不踏实。 - 符号可见性:启动代码里引用的
_sdata、_ebss等符号,必须在链接脚本里用.赋值。如果忘了,链接时会报 undefined reference。
.data 的加载地址写成了 ADDR(.text) + SIZEOF(.text),但忘了考虑对齐。结果 .data 的加载地址和 .text 的结束地址之间差了 2 个字节,拷贝出来的数据全错位了。从那以后,我每个段前后都加 . = ALIGN(4),再也没出过这个问题。
链接脚本这东西,看着不起眼,但它是整个 Bootloader 的地基。地基歪了,上面盖的楼再漂亮也没用。我建议你拿到一个新板子,先把链接脚本调通,确认每个段都落在了正确的位置,再往下写代码。这一步省不了。