Linker Scripts 详解:在裸机上控制内存布局
Source: Dev.to
最小链接脚本的结构
链接脚本主要有两个块:MEMORY 和 SECTIONS。
ENTRY(_start)
MEMORY
{
RAM (rwx) : ORIGIN = 0x60000000, LENGTH = 128M
}
SECTIONS
{
. = 0x60000000;
.text : {
*(.text)
} > RAM
}
每个组件都有其特定的作用。
ENTRY 指令
ENTRY(_start) 指定程序的入口点符号。加载 ELF 文件时,该符号的地址会成为调试器和加载器识别的起始位置。在裸金属代码中,_start 应当对应复位向量转移控制后执行的第一条指令。
注意: 在使用
-kernel启动 vexpress‑a9 时,QEMU 并不会读取架构复位向量,而是把 CPU 的 PC 设置为 ELF 的入口点地址。
MEMORY 块
MEMORY 块声明可用的地址区域及其属性。
| 名称 | 属性 | 起始地址 (ORIGIN) | 长度 (LENGTH) |
|---|---|---|---|
| FLASH | rx | 0x00000000 | 64M |
| RAM | rwx | 0x60000000 | 128M |
MEMORY
{
FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 64M
RAM (rwx): ORIGIN = 0x60000000, LENGTH = 128M
}
属性仅作建议;如果把可写段放入只读区域,链接器会给出警告。
SECTIONS 块
SECTIONS 块指定输出的内存布局。每条记录将输入段(来自目标文件)映射到输出段(最终二进制中),并分配内存位置。
SECTIONS
{
.text : {
*(.text)
} > FLASH
.rodata : {
*(.rodata*)
} > FLASH
}
.text : { … }定义了一个名为.text的输出段。*(.text)包含所有目标文件中的.text输入段。> FLASH把该输出段放入FLASH内存区域。
通配符 (*) 匹配所有输入文件;也可以使用更具体的模式(例如 startup.o(.text))。
位置计数器:.
链接器维护一个隐式变量,称为 位置计数器,用点号 . 表示。它记录当前在内存中的位置,并在布局段时自动递增。
SECTIONS
{
. = 0.60000000; /* 设置位置计数器 */
.text : {
*(.text)
} > RAM /* .text 放在 0x60000000 */
.rodata : {
*(.rodata*)
} > RAM /* .rodata 紧随 .text */
}
也可以基于位置计数器创建符号:
_text_start = .; /* .text 起始地址 */
.text : { *(.text) } > RAM
_text_end = .; /* .text 结束地址 */
这些符号本身不占用存储空间,只是可以在汇编或 C 代码中引用的地址。
VMA 与 LMA:虚拟内存地址与加载内存地址
每个输出段都有两个关联地址:
- VMA(Virtual Memory Address):段在运行时所在的地址。
- LMA(Load Memory Address):段最初存放的地址(通常在非易失性 Flash 中)。
在简单情况下 VMA 与 LMA 相同。对于 .data 等段,初始内容位于 Flash(LMA),但在运行时从 RAM(VMA)执行。启动代码会在使用前把数据从 Flash 复制到 RAM。
.data : {
*(.data)
} > RAM AT > FLASH /* VMA 在 RAM,LMA 在 FLASH */
> RAM 指定 VMA,AT > FLASH 指定 LMA。
链接器定义的符号
链接器可以通过把位置计数器或其他表达式赋给一个名字来创建符号。这些符号仅作为地址存在。
SECTIONS
{
. = 0x60000000;
_text_start = .; /* .text 起始地址 */
.text : { *(.text) } > RAM
_text_end = .; /* .text 结束地址 */
_stack_top = ORIGIN(RAM) + LENGTH(RAM); /* 自定义符号 */
}
在汇编中,这些符号通过 = 伪操作引用:
ldr r0, =_text_start @ 将符号地址装入 r0
ldr sp, =_stack_top @ 将栈顶地址装入 sp
伪操作会生成一条带文字池引用的 LDR 指令,链接器在链接时解析符号地址。
Map 文件
链接器可以生成 map 文件(-Map=output.map),列出段、符号和内存区域的布局。该文件对于确认段是否放在预期位置以及检测大小溢出非常有价值。
验证:链接器的产出
步骤 1:查看反汇编
arm-none-eabi-objdump -d firmware.elf
查找入口点(_start),并确认代码位于期望的地址(例如 0x60000000)。
步骤 2:查看段头信息
arm-none-eabi-objdump -h firmware.elf
输出会显示每个段的 VMA、LMA、大小以及所属的内存区域。
步骤 3:查看 Map 文件
打开 firmware.map,搜索 _text_start、_text_end、_stack_top 等符号。确认它们的地址与脚本中描述的布局相符。
验证:在 QEMU 中加载并使用 GDB 检查
qemu-system-arm -M vexpress-a9 -kernel firmware.elf -nographic -S -s
在另一终端:
arm-none-eabi-gdb firmware.elf
(gdb) target remote :1234
(gdb) info files # 显示已加载的段和入口点
(gdb) break _start
(gdb) continue
检查 PC 是否从 ENTRY(_start) 定义的地址开始,并确认内存区域包含预期的数据。
对齐指令
链接脚本支持对齐指令以满足架构约束:
.text ALIGN(4) : { *(.text) } > FLASH
ALIGN(4) 强制 .text 的起始地址位于 4 字节边界。也可以直接对位置计数器进行对齐:
. = ALIGN(0x1000); /* 对齐到下一个 4 KB 边界 */
正确的对齐可以防止在需要字对齐访问的 CPU 上产生异常。
结论
链接脚本让你能够在裸金属系统中对代码和数据的存放位置进行确定性控制。通过定义内存区域、使用位置计数器、指定 VMA/LMA 并创建链接器定义的符号,你可以匹配硬件所需的精确内存映射。利用反汇编、段头信息、map 文件以及运行时检查(如 QEMU + GDB)来验证输出,能够在烧录设备之前确保布局正确。