4. 链接脚本与内存布局:u-boot.lds 链接脚本分析、代码段/数据段/BSS段布局、重定位地址与运行地址
链接脚本(Linker Script)是嵌入式系统启动流程中承上启下的关键文件。它决定了 U-Boot 的各个段(Section)在内存中的最终排列方式,并直接影响了重定位(Relocation)机制的实现。本章将深入分析 u-boot.lds 的结构,厘清代码段、数据段、BSS 段的布局规则,并重点区分“运行地址”与“重定位地址”这两个核心概念。
4.1 链接脚本的作用与基本结构
链接脚本的主要任务是为链接器(Linker)提供内存布局的指令。在 ARM 嵌入式系统中,U-Boot 的链接脚本通常位于 arch/arm/cpu/u-boot.lds 或 board/<vendor>/<board>/u-boot.lds。它使用 GNU LD 的链接脚本语法,通过 MEMORY 命令定义物理内存区域,并通过 SECTIONS 命令描述各段的输出位置。
一个典型的 U-Boot 链接脚本结构如下:
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
. = 0x00000000; /* 起始链接地址,由具体板级配置决定 */
.text : {
*(.__image_copy_start)
*(.vectors)
arch/arm/cpu/armv7/start.o (.text*)
*(.text*)
}
.rodata : {
*(.rodata*)
}
.data : {
*(.data*)
}
. = ALIGN(4);
.u_boot_list : {
KEEP(*(SORT(.u_boot_list*)));
}
. = ALIGN(4);
__image_copy_end = .;
.rel_dyn_start : {
*(.__rel_dyn_start)
}
.rel.dyn : {
*(.rel*)
}
.rel_dyn_end : {
*(.__rel_dyn_end)
}
.end : {
*(.__end)
}
_image_binary_end = .;
.bss_start : {
*(.__bss_start)
}
.bss : {
*(.bss*)
. = ALIGN(4);
}
.bss_end : {
*(.__bss_end)
}
}
4.2 代码段、数据段与 BSS 段布局
U-Boot 的二进制映像在内存中按照严格的顺序排列。下表总结了各主要段的属性与作用:
| 段名称 | 内容 | 属性 | 是否在映像文件中占用空间 | 初始化方式 |
|---|---|---|---|---|
.text |
异常向量表、启动代码、核心功能函数 | 只读、可执行 | 是 | 由链接器直接填充 |
.rodata |
字符串常量、只读数据 | 只读 | 是 | 由链接器直接填充 |
.data |
已初始化的全局变量和静态变量 | 可读写 | 是 | 由链接器直接填充 |
.u_boot_list |
U-Boot 命令链表、驱动链表等 | 可读写 | 是 | 由链接器排序并填充 |
.rel.dyn |
动态重定位表项 | 只读 | 是 | 由链接器生成 |
.bss |
未初始化或初始化为 0 的全局/静态变量 | 可读写 | 否 | 由 U-Boot 启动代码在运行时清零 |
4.3 运行地址与重定位地址
在 U-Boot 的启动流程中,存在两个关键的地址概念:
- 运行地址(Load Address / Run Address):U-Boot 代码当前正在执行的物理地址。在启动第一阶段,U-Boot 通常从 Flash(如 SPI NOR、NAND)或 ROM 中加载到 SRAM 或片内 RAM 中执行,此时的运行地址由硬件复位向量决定。
- 重定位地址(Relocation Address):U-Boot 在完成第一阶段初始化后,将自身从当前运行位置复制到 DDR 中的目标地址。这个目标地址就是重定位地址。重定位后,U-Boot 会在新地址处继续执行,并访问位于新地址的数据段和 BSS 段。
链接脚本中定义的起始地址(. = 0x00000000)通常是“链接地址”,它并不一定等于最终的运行地址。U-Boot 通过位置无关码(PIC)技术,使得在重定位前代码可以在任意地址执行。重定位过程由 board_init_f 和 relocate_code 函数协同完成。
CONFIG_SYS_TEXT_BASE 与链接脚本的起始地址匹配,并且重定位偏移量计算正确。
4.4 内存布局与重定位流程 SVG 图
下图展示了 U-Boot 从 Flash 加载到最终在 DDR 中重定位完成的完整内存布局变化过程:
4.5 重定位表 .rel.dyn 的作用
U-Boot 的重定位并非简单的内存复制,它还需要修正代码中的绝对地址引用。.rel.dyn 段存储了所有需要重定位的地址条目。每个条目包含:
- 偏移量(Offset):需要修正的地址在映像中的偏移。
- 类型(Type):重定位类型,如
R_ARM_RELATIVE。 - 附加值(Addend):用于计算的基址偏移。
重定位时,U-Boot 遍历 .rel.dyn 表,对每个条目执行:*(偏移量) += 重定位地址 - 运行地址。这样,所有指向全局变量或函数的绝对地址都会被修正为指向 DDR 中的新位置。
.rel.dyn 表是否被正确复制,以及重定位偏移量是否计算正确。使用 md 命令查看 .rel.dyn 段的原始内容,并与链接映射文件(.map)对比,是定位此类问题的有效手段。
4.6 链接脚本中的符号与边界
链接脚本中定义了一系列符号,供启动代码在运行时使用。这些符号在 C 代码中通过 extern 声明后即可访问:
| 符号名称 | 含义 | 典型用途 |
|---|---|---|
__image_copy_start |
映像复制的起始地址 | 重定位时 memcpy 的源地址 |
__image_copy_end |
映像复制的结束地址 | 重定位时 memcpy 的复制长度计算 |
__bss_start |
BSS 段起始地址 | BSS 清零循环的起始点 |
__bss_end |
BSS 段结束地址 | BSS 清零循环的结束点 |
_image_binary_end |
整个二进制映像的结束地址 | 堆栈或堆区域的起始计算 |
__rel_dyn_start |
重定位表起始地址 | 重定位循环的起始点 |
__rel_dyn_end |
重定位表结束地址 | 重定位循环的结束点 |
这些符号的地址在链接时确定,并固化在 ELF 文件中。启动代码通过读取这些符号的值,即可获得各段在内存中的实际边界,从而完成复制、清零、重定位等操作。
4.7 常见问题与调试建议
- 问题:U-Boot 启动后打印“undefined instruction”或“prefetch abort”。
排查:检查链接脚本中.vectors段是否被放置在起始位置,以及异常向量表是否被正确复制到重定位地址。 - 问题:全局变量在重定位后值丢失或错误。
排查:确认.data段被包含在__image_copy_start到__image_copy_end范围内,且 BSS 清零操作在重定位之后执行。 - 问题:重定位后函数指针调用失败。
排查:检查.rel.dyn表是否完整,以及重定位循环是否正确处理了所有R_ARM_RELATIVE条目。
relocate_code 函数的入口和出口。对比重定位前后内存中 .text 段的内容,以及 __image_copy_start 和 __image_copy_end 符号的值,可以快速定位复制或重定位逻辑中的错误。