Armv8架构UBOOT 启动篇——SPL(u-boot-spl.lds链接脚本)

本文深入分析了SPL-UBOOT的启动流程,从链接脚本入手,详细解读了ARMv8架构下SPL-UBOOT的镜像结构,包括内存布局、代码段与数据段的安排,以及如何利用特定段实现代码与数据的重定位。
部署运行你感兴趣的模型镜像

背景

UBOOT版本:2016

架构:Armv8 Cortex A53

DEMO板:freescale ls1043ardb


引言

从uboot 2011版本之后便引入了SPL架构,将之前所有secondary program loader相关的代码实现进行规范,这样也使得各类单板的相关代码实现可以复用,也有效的避免了重复代码和符号链接问题。众所周知,对于不同类型的CPU以及其对应的不同启动方式决定了SPL的实现与否。当单板需要通过SPL启动时,往往由于以下几个因素:

1. 启动存储介质不支持或不能友好的支持XIP,需要将启动代码搬到内部的SRAM中,如NAND、SD卡。

2. CPU内部的SRAM较小,无法装下整个UBOOT镜像,需要一个必要且精简的小镜像先将外部RAM初始化后,再将UBOOT搬到外部RAM进行加载运行。

3. 某些特殊接口类型的启动存储介质如SPL-FLASH,需要对相应SPI控制器进行初始化才能访问。

为此,接下来将对SPL-UBOOT的流程进行分析。

启动流程分析

首先,从spl-uboot的链接脚本入手,分析整个spl-uboot的镜像结构。

链接脚本

armv8架构的spl-uboot的链接脚本如下,接下来便针对该链接脚本的每一部分进行解释和说明。

//路径:u-boot-spl.lds	arch\arm\cpu\armv8	

MEMORY { .sram : ORIGIN = CONFIG_SPL_TEXT_BASE,
		LENGTH = CONFIG_SPL_MAX_SIZE }
MEMORY { .sdram : ORIGIN = CONFIG_SPL_BSS_START_ADDR,
		LENGTH = CONFIG_SPL_BSS_MAX_SIZE }

脚本的最开始定义了两段内存空间,分别为sram和sdram的起始地址和长度。一般来说,这两段空间对应的就是CPU内部的sram和单板外部的sdram如ddr。以ls1043 cpu为例,其定义如下,

#define CONFIG_SPL_TEXT_BASE          0x10000000
#define CONFIG_SPL_MAX_SIZE           0x1d000               /* 116K */
#define CONFIG_SPL_BSS_START_ADDR     0x80100000
#define CONFIG_SPL_BSS_MAX_SIZE       0x80000               /* 512K */

可以看到对于ls1043 cpu来说其定义的spl-uboot两段空间,一段是从0x1000000开始的116K空间,这段空间从相应CPU手册上可以看到是内部RAM中的一段。而0x80100000开始的512K空间则是用来存放未初始化的全局变量和未初始化的静态局部变量的BSS数据段,其位于外部存储即SDRAM如DDR上的一段物理地址空间。

OUTPUT_FORMAT("elf64-littleaarch64", "elf64-littleaarch64", "elf64-littleaarch64")
OUTPUT_ARCH(aarch64)
ENTRY(_start)

紧接着便是指定输出的格式,对于Armv8来说,默认输出的格式就是小端aarch64。Entry用于指定指定入口地址,注意这里使用的是代码中定义的_start符号,该符号定义在start.S    arch\arm\cpu\armv8 最上方,也就是整个SPL-UBOOT的入口,后面会详细描述这部分的内容。

	.text : {
		. = ALIGN(8);
		*(.__image_copy_start)
		CPUDIR/start.o (.text*)
		*(.text*)
	} >.sram

上面这段定义了一段代码段,并指示连接器将其放入到上面定义的.sram内存区域中。其中.ALIGN(8)说明首地址8字节对齐。随后紧接着是存放.__image_copy_start段,该段存放了什么内容呢?我们看一下。

char __image_copy_start[0] __attribute__((section(".__image_copy_start")));

从上面这段代码可以看到,定义了一个0长度也就是不占存储空间的字符数组,并将其规定放置到.__image_copy_start段中,注意该段位于.text段之前。紧接着便是start.o的代码段以及所有其他文件的.text段。这里之所以要将start.o的代码段单独拎出来,主要的目的在于确保start.s文件编译后的代码段位于最终生成spl-uboot文件代码段的最前面。

	.rodata : {
		. = ALIGN(8);
		*(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*)))
	} >.sram

.rodata段用于存放只读数据段。通常,链接器会把匹配的文件和段按照发现的顺序放置,此外可以使用关键字按照一定规则进行顺序修改。其中SORT_BY_ALIGNMENT对段的对齐需求使用降序方式排序放入输出文件中,大的对齐被放在小的对齐前面,这样可以减少为了对齐需要的额外空间。而SORT_BY_NAME关键字则会让链接器将文件或者段的名字按照上升顺序排序后放入输出文件。这里SORT_BY_ALIGNMENT(SORT_BY_NAME(wildcard section pattern)). 先按对齐方式排,再按名字排。

	.data : {
		. = ALIGN(8);
		*(.data*)
	} >.sram

.data数据段主要用于存放全局已初始化的数据段和已初始化的局部静态变量。

	.u_boot_list : {
		. = ALIGN(8);
		KEEP(*(SORT(.u_boot_list*)));
	} >.sram

u_boot_list段用于存放所有的uboot命令,后面在SPL将uboot搬到外部sdram时,注册的搬运函数方法也是存放在这个段里面,我们暂时只看一下添加uboot命令宏具体是怎么展开的。

#define U_BOOT_CMD(_name, _maxargs, _rep, _cmd, _usage, _help)		\
	U_BOOT_CMD_COMPLETE(_name, _maxargs, _rep, _cmd, _usage, _help, NULL)

#define U_BOOT_CMD_COMPLETE(_name, _maxargs, _rep, _cmd, _usage, _help, _comp) \
	ll_entry_declare(cmd_tbl_t, _name, cmd) =			\
		U_BOOT_CMD_MKENT_COMPLETE(_name, _maxargs, _rep, _cmd,	\
						_usage, _help, _comp);
#define U_BOOT_CMD_MKENT_COMPLETE(_name, _maxargs, _rep, _cmd,		\
				_usage, _help, _comp)			\
		{ #_name, _maxargs, _rep, _cmd, _usage,			\
			_CMD_HELP(_help) _CMD_COMPLETE(_comp) }

#define ll_entry_declare(_type, _name, _list)				\
	_type _u_boot_list_2_##_list##_2_##_name __aligned(4)		\
			__attribute__((unused,				\
			section(".u_boot_list_2_"#_list"_2_"#_name)))

举例来说如,

U_BOOT_CMD(
	bootm,	CONFIG_SYS_MAXARGS,	1,	do_bootm,
	"boot application image from memory", bootm_help_text
);

拓展开来就是:

cmd_tbl_t  _u_boot_list_2_cmd_2_bootm  __aligned(4)  __attribute__((unused,                \
            section(".u_boot_list_2_cmd_2_bootm))) =  {"bootm",  CONFIG_SYS_MAXARGS,  1,  do_bootm, "boot application image from memory",  bootm_help_text,  NULL}

tips:##表示连接符  #表示转换为字符串

从上面可以看到实际是定义了一个cmd_tbl_t 结构体变量,并将该变量存放在.u_boot_list_2_cmd_2_bootm段中也就是上面链接脚本指定的u_boot_list段处。

	.image_copy_end : {
		. = ALIGN(8);
		*(.__image_copy_end)
	} >.sram

	.end : {
		. = ALIGN(8);
		*(.__end)
	} >.sram

	_image_binary_end = .;

接下来与__image_copy_start对应,同样定义一个不占空间的空字符数组__image_copy_end用于表示拷贝结尾地址。其实__image_copy_start和__image_copy_end主要作用是用于重定位,即可以知道将什么地址开始,什么地址结束的这一段空间中的代码和数据段进行搬运。并且它们是不占空间的,举例来说,我们知道对于ls1043 cpu 来说,sram起始地址是0x10000000 , 本身就是8字节对齐的,所以的__image_copy_start存放的地址就是0x10000000,而__image_copy_start本身就一个数组名,也即是表示该数组存放的地址,所以其值即为0x10000000。随后定义的__end段以及将当前地址赋值给_image_binary_end全局变量,以便需要时使用。

        .bss_start : {
		. = ALIGN(8);
		KEEP(*(.__bss_start));
	} >.sdram

	.bss : {
		*(.bss*)
		 . = ALIGN(8);
	} >.sdram

	.bss_end : {
		KEEP(*(.__bss_end));
	} >.sdram

最后则是将.bss段规划到外部存储sdram中,并同样定义了两个空字符数组变量__bss_start和__bss_end指定其起始和结束地址。为什么bss段和其他段不同无需搬运?因为首先bss段存放的是未初始化的全局变量和局部静态变量,.bss不占据实际的文件大小,只在段表中记录大小,在符号表中记录符号。当文件加载运行时,才分配空间以及初始化。所以实际.bss段只会在运行时才分配空间,分配的空间起始地址也就是从.sdram定义的空间里面。此外,一般重定向也是将boot拷贝搬移到外部sdram中去运行,而对于ls1043 cpu来说这里bss指定的地址本身就已经在sdram中了。这样也说明了,对于ls1043 cpu来说,要使用bss的数据,需要将外部sdram初始化后才能使用。

	/DISCARD/ : { *(.dynsym) }
	/DISCARD/ : { *(.dynstr*) }
	/DISCARD/ : { *(.dynamic*) }
	/DISCARD/ : { *(.plt*) }
	/DISCARD/ : { *(.interp*) }
	/DISCARD/ : { *(.gnu*) }

最后有几个特殊的输出section名为/DISCARD/被该section引用的任何输入section将不会出现在输出文件内。

结尾

综上,整个链接脚本lds分析完毕。如有不正确的地方,欢迎指出。

 

您可能感兴趣的与本文相关的镜像

Python3.10

Python3.10

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

### 链接脚本的作用 链接脚本(Linker Script)主要用于定义程序的内存布局,包括各个段(section)的地址、大小以及它们之间的关系。它通过指定代码段、数据段和只读数据段等的位置,确保程序能够在目标硬件上正确运行[^1]。在嵌入式系统中,链接脚本的作用尤为重要,因为它直接影响到启动代码的执行流程和内存分配。 对于 U-Boot 而言,链接脚本不仅决定了 SPL(Secondary Program Loader)和 U-Boot 主体的内存分布,还影响了全局数据结构 `global_data` 的初始化位置以及异常向量表的设置[^5]。 --- ### u-boot-spl.lds 文件的功能 `u-boot-spl.lds` 是专为 SPL(Secondary Program Loader)阶段设计的链接脚本文件。以下是其主要功能: #### 1. 定义内存布局 `u-boot-spl.lds` 明确指定了 SPL 的代码段(`.text`)、数据段(`.data`)、只读数据段(`.rodata`)以及 BSS 段(`.bss`)的起始地址和大小。这些信息通常与目标硬件的内存映射密切相关[^2]。 例如: ```ld SECTIONS { . = CONFIG_SYS_TEXT_BASE; .text : { *(.text) } > FLASH .data : { *(.data) } > SRAM .bss : { __bss_start = .; *(.bss) *(COMMON) . = ALIGN(4); __bss_end = .; } > SRAM } ``` 上述代码片段展示了如何将 `.text` 段放置在 Flash 中,而 `.data` 和 `.bss` 段则位于 SRAM 中[^2]。 #### 2. 设置入口点 链接脚本通过 `ENTRY()` 指令指定程序的入口地址。对于 SPL,入口点通常是汇编文件中的 `_start` 标签,例如 `arch/arm/cpu/armv8/start.S` 中的 `_start` 函数[^4]。 示例: ```ld ENTRY(_start) ``` #### 3. 异常向量表 SPL 阶段可能需要响应某些异常中断,因此链接脚本会将异常向量表放置在特定的内存地址上。这通常通过 `. = CONFIG_SYS_SPL_TEXT_BASE;` 和 `.vectors` 段实现。 示例: ```ld .vectors : { KEEP(*(.vectors)) } > SRAM ``` #### 4. 全局数据初始化 `u-boot-spl.lds` 还负责为全局数据结构 `global_data` 分配内存空间,并确保其在 SPL 阶段被正确初始化[^5]。 --- ### 示例分析 结合 Armv8 架构下的 SPL 启动流程,`u-boot-spl.lds` 的功能可以进一步说明如下: 1. **选择 SP_EL1 堆栈** 在 Armv8 架构中,SPL 阶段需要切换到 EL1 层级并选择 SP_EL1 堆栈。链接脚本通过定义 `.bss` 段的位置,确保堆栈指针能够正确初始化。 2. **跳转到 C 代码入口** 链接脚本中的 `.text` 段包含了从汇编代码跳转到 C 代码入口 `_main` 的逻辑。这一步骤是 SPL 启动过程中的关键环节。 3. **支持多核启动** 如果目标硬件支持多核处理器,链接脚本还需要为每个 CPU 核心分配独立的堆栈空间[^2]。 --- ### 总结 链接脚本在 U-BootSPL 阶段扮演了至关重要的角色,它不仅定义了内存布局,还确保了程序能够正确加载和执行。`u-boot-spl.lds` 特别针对 SPL 阶段的需求进行了优化,包括异常向量表的设置、入口点的定义以及全局数据的初始化。 ---
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值