U-Boot分析【学习笔记】(7)

8. 链接脚本分析

U-Boot分析【学习笔记】(6)中,我们通过追踪 U-Boot 的顶层目标 _all 的依赖链,理清了生成 u-boot.bin 所需的全部原材料。我们已经知道:

libs-y 收集了各个子目录的零件。

u-boot-main 汇总了这些零件。

u-boot (ELF) 最终将它们缝合。

但这里隐藏着一个关键问题:

所有的二进制零件(.o 文件)虽然被链接在了一起,但它们在生成的 16 进制镜像里,谁排在第 1 个字节?谁排在第 1024 个字节? 这就是所谓的 内存摆放 。对于 i.MX6ULL 这种 ARM 处理器,上电后它会从固定地址读取指令。如果启动代码被排在了镜像的中间或末尾,CPU 读到的就是一堆乱码。链接脚本 (u-boot.lds) 的唯一任务,就是给 Makefile 收集到的所有逻辑零件,安排一个绝对的物理位置坐标。

8.1 u-boot.lds 分析

在主 Makefile 中,发现 u-boot (ELF) 的生成规则里,除了刚才提到的 u-boot-main,还有一个至关重要的依赖项:

shell 复制代码
u-boot: $(u-boot-init) $(u-boot-main) u-boot.lds FORCE
    $(call if_changed,u-boot__)

在 ARM 架构中,它通常位于:
arch/arm/cpu/u-boot.lds

为什么要研究它?

Makefile 的依赖链告诉我们'要编什么',而链接脚本则告诉链接器'怎么排序'。理解了 .lds,才真正理解了程序是如何在 i.MX6ULL 的 512MB DDR 内存中分配位置的。

入口与格式设定

c 复制代码
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)

分析:

这里规定了输出文件的格式为小端模式的 32 位 ARM ELF 文件。
架构意义:
ENTRY(_start) 是整份文件的核心 ,它指定了程序的 物理入口点。

当 CPU 完成基础自检后,指针跳向的第一个符号就是 _start

置计数器(.)与起始地址

c 复制代码
. = 0x00000000;
. = ALIGN(4);

分析:
. 是当前位置计数器:

它代表链接器当前正准备摆放数据的"内存地址指针"。可以把它想象成电脑屏幕上的光标,光标在哪,字就打在哪;链接脚本里的 . 在哪,代码就从哪个地址开始排。
0x00000000:

相对于链接起始地址(CONFIG_SYS_TEXT_BASE)的偏移。
ALIGN(4):

"接下来的内容,必须从能被 4 整除的内存地址开始摆放。"
举例:

假设链接器刚刚放完了一段数据,现在的指针 . 停在了地址 0x87800003。

执行 ALIGN(4):计算发现比 0x87800003 大、且能被 4 整除的最近地址是 0x87800004。

执行赋值 .:指针跳到 0x87800004。

结果:中间那个 0x87800003 的位置就被空出来了(填充为 0 或空),下一段代码将从稳妥的 0x87800004 开始排放。
架构意义:

在 i.MX6ULL 中,这个起始地址通常被配置为 0x87800000。ARM 指令集通常要求指令地址必须是对齐的(例如 32 位 ARM 指令必须 4 字节对齐)。如果不对齐,CPU 根本无法取指运行。
链接器以此为原点,开始计算后续所有代码和数据的绝对物理坐标。

8.2 .text 代码段

代码段(.text)的强制排序

c 复制代码
.text :
{
    *(.__image_copy_start)
    *(.vectors)
    CPUDIR/start.o (.text*)
    *(.text*)
}

要理解这段代码说了什么,要先知道什么是代码段

代码段(.text 段)是程序在内存中专门用来存放 动作指令 的物理区域

  1. 它是"指令"而非"数值":代码段里存的是 MOV(搬移)、ADD(加法)、B(跳转)等告诉 CPU 该干什么的二进制命令。
  2. 它是"只读"的:为了安全,硬件通常不允许程序在运行时修改代码段。
  3. 它是"连续"的:链接器会把散落在各个 .o 文件里的代码块首尾相连。
    最外层 .text : { ... } 代表什么?

在链接脚本的语境下,这是一个"段定义"。
.text (段名):

这是给输出文件(生成的 u-boot)定义的段名称。

正如我们之前讨论的,它代表"代码段",用来存放所有的机器指令。
冒号 :

这是一个赋值声明,告诉链接器:"要开始定义这个段的内部组成和存放规则"。

{ ... } :

它是一个逻辑容器。它告诉链接器:"凡是放在这对花括号里的东西,在最终的二进制镜像中,物理上必须是连续排列在一起的 "。

它也代表了地址空间。花括号内的所有内容,都会共享 .text 这个大段的起始地址属性。
语法说明:

shell 复制代码
*(.text*)
  1. 外层的 * :文件通配符 (File Wildcard)
    含义:代表 "所有输入文件"。
    原理:链接器会扫描工程编译出的所有 .o 目标文件(.o 文件是链接器的标准零件 )。
    架构作用:它告诉链接器把整个项目里所有满足括号内条件的段都找出来。
  2. 括号 ( ) :作用域定界符
    含义:括号将 "目标文件" 和 "目标段" 进行逻辑关联。
    原理:括号左边是文件(这里是通配符 *),括号内部是这些文件里的具体段。
    架构作用:这是一种筛选语法,结构为 文件名 (段名)。
  3. 内部的 .text* :段名匹配 (Section Pattern)
    这里要拆成两部分看:
    .text:代表 代码段。这是编译器约定的名字,存放的是 CPU 可以执行的机器指令。
    末尾的 *:代表 后缀匹配。
  4. 为什么要加这个
    当编译器开启优化选项(如 -ffunction-sections)时,它为了减小最终体积,会把每个函数单独放进一个段,名字叫 .text.函数名(例如 .text.main)。
    结果:如果只写 (.text),链接器可能漏掉那些带后缀的函数段。写成 (.text
    ) 就能确保把所有的指令段 "一网打尽"。
  5. 完整含义是:
    扫描 所有输入文件 (),从中提取出 所有以 .text 开头的代码段 (.text),并将它们按照链接顺序,整齐地摆放在当前输出文件的这个位置。
c 复制代码
*(.__image_copy_start)

含义:

这是一个特殊的符号标记段,它不包含任何指令(不占实际空间 ),但它是一个锚点。把它放在 .text 的最顶端,是为了让 __image_copy_start 这个标签的地址,正好指向 U-Boot 在内存中存储的首个字节。
架构作用:

当 U-Boot 运行到一半需要换位置(Relocation)时,从这个符号标记的起始位置开始拷贝数据。

c 复制代码
*(.vectors)

含义:

由汇编语言定义的中断向量表 ,对于 ARM 处理器(如 i.MX6ULL),硬件规定在发生复位(Reset)、未定义指令或中断时,CPU 会强制跳转到内存的最起始偏移量(通常是 0x00)去寻找指令。
架构作用:

如果这里放的不是向量表而是普通代码,当系统发生中断时,CPU 会跳到一段乱码中去执行,导致直接死机

c 复制代码
CPUDIR/start.o (.text*)

含义:

这是指定的 start.o 目标文件(由 start.S 编译而来)里的代码段。

这是整个系统的入口点。它包含了关闭看门狗、初始化 DDR、设置堆栈等裸机最基础的操作。
架构作用:

虽然 Makefile 收集了成千上万个 .o,但 LDS 规定:start.o 必须排在通用代码之前。

逻辑联系:因为 start.S 包含了 U-Boot 的第一行指令 _start。如果它不排在前面,CPU 跳进来执行的就是其他的函数,系统直接崩溃。

c 复制代码
*(.text*)

含义:

这是工程中剩下所有 .o 文件里的代码段。

这里汇聚了写的所有 C 语言逻辑、网络协议栈、文件系统、驱动程序等
语法:

外层 :代表"所有文件"。
内层 .text
:匹配所有以 .text 开头的标签(包括编译器为了优化而贴上的 .text.delay、.text.main 等花名)。
架构作用:

这些代码依赖前面的 start.o 搭建好的硬件环境(如内存已经初始化好、栈已经设好)。只有前面的基础打好了,后续代码才能正常运行。
text 的关联
最外层的 .text (段名):

它是输出段(Output Section)。

它代表最终生成的 u-boot 二进制镜像中,那块被标记为"代码"的巨大区域。
内部的 CPUDIR/start.o (.text *) 和 (.text *):**

它们是输入段(Input Section)。它们是分散在各个 .o 零件里的碎块。
为什么都叫 text?

这其实是一种"标签匹配"的契约:

对编译器(汇编器)而言:它把代码翻译成二进制时,默认贴上 .text 的标签。所以 start.o 里面有 .text,main.o 里面也有 .text。

对链接脚本而言:它通过内部的 (.text*) 这种语法,像吸铁石一样寻找所有带 .text 标签的碎块。

对输出段名(最外层)而言:可以把它改成 .my_code,但为了遵循行业标准(ELF 规范),我们通常也把它命名为 .text。
总结:

最外层叫 .text 是为了告诉操作系统/硬件"这一块是代码";内部写 .text* 是为了告诉链接器"去把那些叫 .text 的零件找回来"。

8.3 镜像资源布局与物理边界控制

.rodata:存放"只读数据"

c 复制代码
. = ALIGN(4);
.rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }

什么是 .rodata (Read-Only Data):

它存放的是程序中不可修改的常量。
例子:

在 C 代码里写的字符串 printf("Hello World"),这个 "Hello World" 就会被编译器贴上 .rodata 标签存放在这里。

SORT_BY_NAME 和 SORT_BY_ALIGNMENT:

这是链接器的"强迫症"功能。它会对所有收集到的 .rodata 碎块先按名字排序,再按字节对齐排序。
架构作用:

将常量与代码指令分开。虽然它们都是只读的,但在物理上分开存放有利于链接器进行管理和空间压缩。
.data:存放"已初始化全局变量"的仓库

c 复制代码
. = ALIGN(4);
.data : {
   *(.data*)
}

什么是 .data (Data):

它存放的是你定义的初始值不为 0 的全局变量。
例子:

int a = 100;(全局变量)。这个 100 必须实实在在地存在二进制文件里。
对比理解:

代码段 (.text):告诉 CPU "如何加法"。

数据段 (.data):告诉 CPU "被加的数起始值是 100"。
架构作用:

这是镜像中占用体积的一部分。U-Boot 启动时,会将这部分数据随代码一起从 Flash 拷贝到 RAM 中,因为程序运行过程中会修改这些变量的值(比如计数器、状态标志等)。
u_boot_list:U-Boot 的"特殊功能名单"

c 复制代码
.u_boot_list : {
   KEEP(*(SORT(.u_boot_list*)));
}

这是什么 (最具 U-Boot 特色):

这是 U-Boot 用来实现 "命令自动发现" 等功能的黑科技。
KEEP 关键字:

链接器有个习惯:如果发现某段代码没被调用,就会把它删掉。

但 .u_boot_list 里的数据往往是给底层框架用的,看似没有用。KEEP 强制链接器,禁止删除。
架构作用:

当你使用 U_BOOT_CMD 定义一个命令(比如 ls 或 nfs)时,编译器会把命令结构体丢进这个段。
结果:

U-Boot 运行起来后,只需要遍历这个内存段,就能知道当前系统支持哪些命令。它实现了模块之间的 "解耦"------增加一个命令文件,不需要去修改主代码,链接器会自动把它缝合进这个名单里。


EFI 代码与数据
定义的是 U-Boot 中专门用于 UEFI 运行时服务的特殊区域

c 复制代码
	. = ALIGN(4);

	.__efi_runtime_start : {
		*(.__efi_runtime_start)
	}

	.efi_runtime : {
		*(efi_runtime_text)
		*(efi_runtime_data)
	}

	.__efi_runtime_stop : {
		*(.__efi_runtime_stop)
	}

	.efi_runtime_rel_start :
	{
		*(.__efi_runtime_rel_start)
	}

	.efi_runtime_rel : {
		*(.relefi_runtime_text)
		*(.relefi_runtime_data)
	}

	.efi_runtime_rel_stop :
	{
		*(.__efi_runtime_rel_stop)
	}

1.EFI 代码与数据(核心内容)

c 复制代码
.__efi_runtime_start : { *(.__efi_runtime_start) }  // 起点:围墙开头
.efi_runtime : {
    *(efi_runtime_text) // UEFI 专用代码指令
    *(efi_runtime_data) // UEFI 专用全局变量
}
.__efi_runtime_stop : { *(.__efi_runtime_stop) }   // 终点:围墙结尾

什么是 EFI Runtime?

当 U-Boot 引导操作系统(如 Linux)后,U-Boot 大部分代码都会从内存中消失。但有一部分代码需要继续留在内存里,供操作系统调用(比如设置主板时间、修改启动项),这就是"运行时服务"。
为什么要单独放?

因为它和普通 U-Boot 代码的"寿命"不同。

普通代码引导完就没用了,而这块区域必须在 Linux 运行时依然保持有效。
.__efi_runtime_start 和.__efi_runtime_stop
定义:

在链接脚本(LDS)中,这种带有 _start 和 _stop 后缀的写法,是嵌入式底层开发中极其经典且重要的"边界定义"技术。

简单来说,这两行代码并不包含具体的业务逻辑,它们是为 EFI 运行时服务(Runtime Services) 划定的 "物理围墙"。
作用:

  1. 精准定位大小:
    程序在运行时可以通过 .__efi_runtime_stop - .__efi_runtime_start 计算出这块特区到底占了多少字节。
  2. 整块操作(搬运/保护):
    在 C 语言代码里,我们无法直接知道一个"段"到底占用了内存的哪些地址。但通过这两行定义的符号(Symbols),我们就可以在代码里直接引用

2.EFI 重定位表

c 复制代码
.efi_runtime_rel_start : { *(.__efi_runtime_rel_start) } // 起点
.efi_runtime_rel : {
    *(.relefi_runtime_text)
    *(.relefi_runtime_data)
}
.efi_runtime_rel_stop : { *(.__efi_runtime_rel_stop) }   // 终点

为什么需要"Rel" (Relocation)?

因为操作系统加载后,可能会把这块 EFI 特区移动到内存的其他位置。
作用:

这段区域存放的是"修正手册"。如果特区移动了,它告诉系统如何修正代码里的地址(比如某个变量原来在 A,现在搬到了 B,代码里的指针得跟着改)。
例子

假设在 EFI 特区(efi_runtime_data 段)里定义了一个全局变量,并在函数里引用它:

c 复制代码
// 1. 数据段里的变量(假设原地址在 0x8780_1000)
int efi_status = 1; 

// 2. 代码段里的引用
void check_status(void) {
   if (efi_status == 1) { ... }
}

如果没有重定位:

编译器翻译 check_status 时的机器码逻辑是:

"去内存地址 0x8780_1000 取出那个数,看看是不是 1。"

移动后(EFI 特区整体移动):

操作系统整个特区搬到了 0x9000_1000。

变量 efi_status 现在实际在:0x9000_1000。

致命问题:代码里的指令依然硬编码着:"去 0x8780_1000 取数!"。

后果:CPU 去老地址取数,取到的是一堆垃圾数据,程序直接崩溃。


与起点的__image_copy_start遥相呼应

c 复制代码
	. = ALIGN(8);

	.image_copy_end :
	{
		*(.__image_copy_end)
	}

. = ALIGN(8); ------ 为什么是 8 字节对齐?

  1. 硬件性能要求:
    现代 64 位 CPU 或某些高性能 32 位总线(如 ARM 的 LDRD/STRD 指令,一次操作 64 位数据)要求内存地址必须能被 8 整除。
  2. 拷贝效率:
    U-Boot 搬家时,通常会使用优化过的汇编拷贝函数。这些函数为了追求极致速度,往往会以 8 字节(双字) 为一组进行强制搬运。
    如果没有 8 字节对齐:最后一次搬运可能会跨越边界,甚至漏掉几个字节,或者导致 CPU 触发"对齐异常"。

架构作用:

它在镜像的末尾强行插入一些"填充字节,确保整个 U-Boot 镜像的大小是 8 的倍数。
.image_copy_end:

它是一个空段,不存放任何指令,只存放了一个在汇编(通常是 sections.c 或相关汇编文件)中定义的符号标签 __image_copy_end
作用:

在 U-Boot 的链接脚本开头,分析过一个 __image_copy_start。

现在,这个 __image_copy_end 就是它的另一半。

  1. 划定搬家范围:
    U-Boot 的重定位逻辑非常简单。它会计算:KaTeX parse error: Expected group after '_' at position 8: 搬运长度 = _̲_image_copy_end...只要在这个范围内的东西(代码、只读数据、已初始化变量、EFI特区),通通都会被搬到 DDR 的高地址去。
  2. 避开非搬运区:
    在 __image_copy_end 之后,通常紧跟着的是 .rel.dyn(重定位表)和 .bss(未初始化变量区)。BSS 段不需要拷贝:因为 BSS 里全是 0,拷贝它纯属浪费时间。U-Boot 搬完家后,会直接手动把 BSS 区清零。

8.4 U-Boot 的重定位表

c 复制代码
	.rel_dyn_start :
	{
		*(.__rel_dyn_start)
	}

	.rel.dyn : {
		*(.rel*)
	}

	.rel_dyn_end :
	{
		*(.__rel_dyn_end)
	}

之前分析的 efi_runtime_rel 看作是某个特区的重定位表,那么这里的 .rel.dyn 就是整个 U-Boot 镜像的重定位表。

什么是 Rel.dyn (Dynamic Relocation)?

U-Boot 是一段可以在内存中任意位置运行的代码(位置无关代码)。但在 C 语言里,很多全局变量、函数指针的地址在编译时是写死的。
意义:

当 U-Boot 决定从 Flash 搬移到 DDR 的高地址运行(Relocation)时,它会查阅这张表。表里记录了镜像中所有存放"绝对地址"的位置。

例子:镜像里 0x100 处存了一个地址 0x87800000。搬家后,U-Boot 会遍历这张表,找到 0x100 这个位置,把里面的值加上搬家的位移量,使其变成新地址。

8.5 .end

c 复制代码
.end :
{
    *(.__end)
}

这是 U-Boot 镜像逻辑上的最后一个段。
物理意义:

它标志着所有"有实质内容"的代码和数据到此全部结束。通常紧跟在重定位表后面。在某些内存管理逻辑中,它被用来计算整个 U-Boot 运行所需的最小内存颗粒度。
范围界定:

利用 _start 和 _end 标签,U-Boot 成功地将这份庞大的修正清单封装成一个可识别的整体,确保了搬家过程中的'地址对齐'与'数据完整'。

8.6 BSS段

c 复制代码
	.bss_start __rel_dyn_start (OVERLAY) : {
		KEEP(*(.__bss_start));
		__bss_base = .;
	}

	.bss __bss_base (OVERLAY) : {
		*(.bss*)
		 . = ALIGN(4);
		 __bss_limit = .;
	}

	.bss_end __bss_limit (OVERLAY) : {
		KEEP(*(.__bss_end));
	}
  1. 什么是 BSS 段?(无中生有的艺术)

    定义:BSS 段(Block Started by Symbol)存放的是程序中未初始化或初始化为 0 的全局变量。
    例子:

    int global_array[1024];。这个数组很大,但全是 0。
    特性:

    不占 Flash 空间:既然全是 0,没必要在 u-boot.bin 文件里存 1024 个零。运行时占内存:当程序跑起来后,必须在 RAM 里给它留出位置,并由程序手动清零。

    结论:BSS 是一个"虚"段,它在烧录文件里不存在,只在运行版图中存在。

  2. OVERLAY(覆盖)机制:

    这是这段代码的核心:.bss_start __rel_dyn_start (OVERLAY)。

    为什么可以重叠?(逻辑闭环)
    阶段 1:

    重定位。U-Boot 刚跳到 DDR 时,第一件事是读取 重定位表 (.rel.dyn)。这时候重定位表是"宝库",必须存在。
    阶段 2:

    重定位完成。一旦地址修正好了,重定位表就成了"废纸",占着内存纯属浪费。
    阶段 3:

    清零 BSS。此时,U-Boot 需要一块干净的内存给全局变量(BSS)。
    U-Boot 的做法:

    直接把 BSS 段的起始地址设在重定位表的起始地址(__rel_dyn_start)上。

_start、_base 和 _limit 来确保清零操作的精确性:

.bss_start:锚定重定位表的起点,作为 BSS 的逻辑开头。

.bss:收集所有 .o 文件里的未初始化变量。

.bss_end:标记 BSS 的物理终点。

架构作用:

在 U-Boot 的 board_init_f 阶段,会有一段简单的循环代码(通常在 crt0.S 中):"从 __bss_start 开始,一直到 __bss_end,把这块内存全部抹成 0。"

这一抹,就正式宣告了重定位表的彻底消失,和 BSS 段的正式启用。

相关推荐
www.021 小时前
通过 SSH 隧道将 GPT 调教为服务器专属 Agent(个人记录)
linux·服务器·vscode·gpt·大模型·ssh·api转发
张小姐的猫1 小时前
【Linux】多线程(中)—— 线程控制接口 | 线程库 | 线程局部存储
linux·运维·服务器
脆皮炸鸡7551 小时前
大山之二:文件系统(Ext系列)
linux·开发语言·经验分享·学习方法
打工人小夏1 小时前
使用finalshell在新服务器上部署前端页面
linux·服务器·前端·vue.js
Zhu7581 小时前
软件更新-openssh和openssl-centos
linux·运维·centos
故事还在继续吗1 小时前
嵌入式Linux基础知识
linux·运维·服务器
Huanzhi_Lin1 小时前
skynet笔记
笔记·lua·skynet·actor·actor模型
idolao2 小时前
CentOS 7 安装 httpd-2.4.1.tar.gz 详细步骤(源码编译、配置、启动)
linux·运维·centos
wangjialelele3 小时前
Linux mmap 机制:从 read/write 底层流程到手写 malloc 内存分配
linux·运维·服务器·mmap