背景
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分析完毕。如有不正确的地方,欢迎指出。