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 命令描述各段的输出位置。

核心概念:链接脚本中的地址是“链接地址”(Link Address),即编译阶段确定的虚拟地址。对于 U-Boot 这种裸机程序,链接地址通常等于物理地址(MMU 未开启时),但重定位后,程序的实际物理地址会发生变化。

一个典型的 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 启动代码在运行时清零
调试要点:BSS 段在映像文件中不占用空间,但运行时必须分配内存。如果启动代码中遗漏了 BSS 清零操作,全局变量将包含随机值,导致命令解析、驱动初始化等环节出现不可预知的错误。

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 函数协同完成。

常见陷阱:如果链接脚本中的链接地址与实际的 DDR 重定位地址不一致,且代码中使用了绝对地址访问(如直接引用全局变量的地址),则重定位后程序会崩溃。必须确保 CONFIG_SYS_TEXT_BASE 与链接脚本的起始地址匹配,并且重定位偏移量计算正确。

4.4 内存布局与重定位流程 SVG 图

下图展示了 U-Boot 从 Flash 加载到最终在 DDR 中重定位完成的完整内存布局变化过程:

U-Boot 内存布局与重定位流程 阶段一:Flash 中的映像 .text (向量表 + 启动代码) .rodata / .data .rel.dyn 重定位表 .bss (不占空间) SPL / 加载器 阶段二:SRAM 中运行 (运行地址) .text (PIC 代码) .rodata / .data (小) .rel.dyn 重定位表 .bss (已清零) 重定位 relocate_code() 阶段三:DDR 中运行 (重定位地址) .text (已复制) .rodata / .data .rel.dyn (已修正) .bss (重新清零) 关键说明: 1. 运行地址:U-Boot 在 SRAM 中实际执行的地址,由硬件复位向量决定。 2. 重定位地址:U-Boot 复制到 DDR 后的目标地址,由 CONFIG_SYS_TEXT_BASE 定义。 地址映射关系: Flash 0x08000000 SRAM 0x20000000 DDR 0x80000000

4.5 重定位表 .rel.dyn 的作用

U-Boot 的重定位并非简单的内存复制,它还需要修正代码中的绝对地址引用。.rel.dyn 段存储了所有需要重定位的地址条目。每个条目包含:

  • 偏移量(Offset):需要修正的地址在映像中的偏移。
  • 类型(Type):重定位类型,如 R_ARM_RELATIVE。
  • 附加值(Addend):用于计算的基址偏移。

重定位时,U-Boot 遍历 .rel.dyn 表,对每个条目执行:*(偏移量) += 重定位地址 - 运行地址。这样,所有指向全局变量或函数的绝对地址都会被修正为指向 DDR 中的新位置。

调试技巧:如果重定位后 U-Boot 执行到某个命令时崩溃,可以检查 .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 条目。
硬件调试建议:在调试器(如 GDB + JTAG)中,设置硬件断点在 relocate_code 函数的入口和出口。对比重定位前后内存中 .text 段的内容,以及 __image_copy_start 和 __image_copy_end 符号的值,可以快速定位复制或重定位逻辑中的错误。