armv8架构u-boot启动流程
阅读数:352 评论数:0
跳转到新版页面分类
硬件/嵌入式
正文
一、概述
官方将启动分为了BL1,BL2,BL31,BL32,BL33阶段,根据顺序,芯片启动后首先执行BL1阶段代码,接着验签启动BL2,BL2根据具体设计启动BL31或者BL33,BL32只有在有BL31时才可能会存在并被验签加载启动。
BL1 | 一般就是固化在ROM中的一段启动加载代码,用于引导bl2,并对bl2进行验签保证可信任执行 |
BL2 | 它的可信建立在bl1对它的验证,主要完成一些平台相关的初始化,比如对ddr的初始化等,并在完成初始化后寻找BL31或者BL33进行执行;如果找到了BL31则不会继续调用BL33,如果没有BL31则BL33必须有 |
BL31 | BL31不像BL1和BL2是一次性运行的,在系统运行时通过smc指令陷入EL3调用系统安全服务或者在Secure World和Non-Secure World之间进行切换;在完成BL31初始化后会去寻找BL32或者BL33进行验签后加载执行; |
BL32 | 它是一个可信安全的OS运行在EL1并在EL0启动可信任APP(上述的指纹验证等app),并在Trust OS运行完成后通过smc指令返回BL31,BL31切换到Non-Seucre World继续执行BL33; |
BL33 | 非安全固件,也就是我们常见的UEFI firmware或者u-boot也可能是直接启动Linux kernel; |
armv8分为Secure World和Non-Secure World(Normal World),四种异常级别从高到低分别为EL3,EL2,EL1,EL0。
Secure World | 就是可以执行可信的fireware和app,比如密码支付,指纹识别等一系列依赖安全保证的服务。 |
Non-Secure World | 就是我们常见的u-boot,linux,qnx等裸机程序或者操作系统。 |
EL3 | 具有最高管理权限,负责安全监测和安全模式切换 |
EL2 | 主要提供了对虚拟化的支持。 |
EL1 | 是一个特权模式,能够执行一些特权指令,用于运行各类操作系统,在安全模式则是可信任OS。 |
EL0 | 是无特权模式,所有APP应用都在EL0 |
综上所知,u-boot是一个运行在非安全世界的bootloader,负责加载各类操作系统,并提供丰富的驱动接口;并根据是否存在安全固件还可以进行不同的boot流程。
二、u-boot-spl u-boot-tpl
spl | Secondary Program Loader,二级加载器 |
tpl | Tertiary Program Loader,三级加载器 |
出现spl和tpl的原因最开始是因为系统sram太小,rom无法在ddr未初始化的情况下一次性把所有代码从flash,emmc,usb等搬运到sram中执行。所以u-boot又定义了spl和tpl,spl和tpl走u-boot完全相同的boot流程,不过在spl和tpl中大多数驱动和功能被去除了,根据需要只保留一部分spl和tpl需要的功能,通过CONFIG_SPL_BUILD和CONFIG_TPL_BUILD控制;一般只用spl就足够了。
三、u-boot编译配置方式
u-boot使用了同Linux一样的编译配置方式,即使用kbuild系统来管理整体代码的配置和编译,通过defconfig来定制各种不同厂商的芯片bootloader二进制程序。
编译只需要注意通过环境变量或者命令行参数的方式引入一个交叉编译工具即可:
CROSS_COMPILE | 定义交叉编译工具链,可以是aarch64-linux-gnu-,arm-none-eabi-或者ppc-linux-gnu-等等; |
1、针对board配置
SYS_ARCH,SYS_CPU,SYS_SOC,SYS_BOARD,SYS_VENDOR,SYS_CONFIG_NAME;
一般在board/vendor/board/Kconfig中可全部定义,部分SYS_CPU,SYS_SOC也可以在arch/xxx/Kconfig中定义,根据这几个配置即可确定使用的cpu架构,厂商,板级信息,soc信息。Makefile会自动根据上述信息进入对应目录组织编译规则,一般如果没有自己对应的这些board信息,需要自己在对应目录建立这些Kconfig和在configs中建立defconfig。
在configs目录中保存了uboot中所有支持的board配置,比如要使用rk3399的evb板的配置信息使用如下方式即可编译出来:
make CROSS_COMPILE=aarch64-linux-gnu- evb-rk3399_defconfig
make
如果没有对应的defconfig可以找一个与自己板级信息类似的defconfig生成一个.config,再通过menuconfig来完成自己board的配置,并最后通过savedefconfig保存为自己board的defconfig:
make CROSS_COMPILE=aarch64-linux-gnu- evb-rk3399_defconfig
make menuconfig
make savedefconfig
cp defconfig configs/my_defconfig
根据CONFIG_SYS_BOARD的定义还会为每个源文件自动包含include/configs/xxxx.h头文件,evb rk3399则是include/configs/evb_rk3399.h头文件。
这个头文件可在其中定义board的一些关键配置,系统的ram大小,环境变量的起始地址和大小,GIC基地址,时钟频率,是否开启看门狗等定义,可根据具体需求来定义。
四、u-boot源码结构
1、常用目录
arch | 各种架构的启动初始化流程代码,链接脚本等均在此目录对应的架构中存放; |
board | 包含了大部分厂商的board初始化代码,基本平台化相关的代码都在对应的board目录中,早期的一些board代码在arch/xxx/xxx-mach中,现在基本不会放在arch目录下面了; |
cmd | 包含了大量实用的u-boot命令的实现,比如md,cp,cmp,tftp,fastboot,ext4load等命令的实现,我们也可以在此处添加自己实现的命令 |
common | 包含了u-boot的核心初始化代码,包括board_f,board_r,spl等一系列代码; |
configs | 包含了所有board的配置文件,可直接使用; |
drivers | 大量驱动代码的存放处; |
dts | 编译生成dtb,内嵌dtb到u-boot的编译规则定义目录; |
env | 环境变量功能实现代码; |
fs | 件系统读写功能的实现,里面包含了各类文件系统的实现; |
include | 所有公用头文件的存放路径; |
lib | 大量通用功能实现,提供给各个模块使用; |
net | 网络相关功能的实现; |
scripts | 编译,配置文件的脚本文件存放处; |
tools | 测试和实用工具的实现,比如mkimage的实现代码在此处; |
五、armv8链接脚本
通过链接脚本可以从整体了解一个u-boot的组成,并且可以在启动分析中知道某些逻辑是在完成什么工作。在armv8中,u-boot使用arch/arm/cpu/armv8/u-boot.lds进行链接。u-boot-spl和u-boot-tpl使用arch/arm/cpu/armv8/u-boot-spl.lds进行链接,因为每个board的情况可能不同,所以u-boot可以通过Kconfig来自定义u-boot-spl.lds和u-boot-tpl.lds。
1、u-boot.lds
/* SPDX-License-Identifier: GPL-2.0+ */
/*
* (C) Copyright 2013
* David Feng <fenghua@phytium.com.cn>
*
* (C) Copyright 2002
* Gary Jennejohn, DENX Software Engineering, <garyj@denx.de>
*/
#include <config.h>
#include <asm/psci.h>
OUTPUT_FORMAT("elf64-littleaarch64", "elf64-littleaarch64", "elf64-littleaarch64")
OUTPUT_ARCH(aarch64)
ENTRY(_start) -------------------------------------------------------------------- (1)
/*
*(1)首先定义了二进制程序的输出格式为"elf64-littleaarch64",
* 架构是"aarch64",程序入口为"_start"符号;
*/
SECTIONS
{
#ifdef CONFIG_ARMV8_SECURE_BASE -------------------------------------------------- (2)
/*
*(2)ARMV8_SECURE_BASE是u-boot对PSCI的支持,在定义时可以将PSCI的文本段,
* 数据段,堆栈段重定向到指定的内存,而不是内嵌到u-boot中。
* 不过一般厂商实现会使用atf方式使其与bootloader分离,这个功能不常用;
*/
/DISCARD/ : { *(.rela._secure*) }
#endif
. = 0x00000000; -------------------------------------------------------------- (3)
/*
*(3)定义了程序链接的基地址,默认是0,通过配置CONFIG_SYS_TEXT_BASE可修改
* 这个默认值。
*/
. = ALIGN(8);
.text :
{
*(.__image_copy_start) --------------------------------------------------- (4)
/*
*(4)__image_copy_start和__image_copy_end用于定义需要重定向的段,
* u-boot是一个分为重定向前初始化和重定向后初始化的bootloader,
* 所以此处会定义在完成重定向前初始化后需要搬运到ddr中数据的起始地址和结束地址;
*
* 大多数时候u-boot是运行在受限的sram或者只读的flash上,
* u-boot为了启动流程统一会在ddr未初始化和重定位之前不去访问全局变量,
* 但是又为了保证u-boot能够正常读写全局变量,内存,调用各类驱动能力,
* 所以u-boot将启动初始化分为了两个部分,重定向前初始化board_f和
* 重定向后初始化 board_r,在重定向之前完成一些必要初始化,
* 包括可能的ddr初始化,然后通过__image_copy_start和__image_copy_end
* 将u-boot搬运到ddr中,并在ddr中进行重定向后初始化,这个时候的u-boot就可以
* 正常访问全局变量等信息了。
*
* 如果想要在board_f过程中读写一些全局变量信息该怎么办呢?
* u-boot通过定义global_data(gd)来完成此功能,
* 后续在分析到时会详细讲解实现方式。
*/
CPUDIR/start.o (.text*) -------------------------------------------------- (5)
/*
*(5)定义了链接程序的头部文本段,armv8就是
* arch/arm/cpu/armv8/start.S,
* start.S中所有文本段将会链接到此段中并且段入口符号就是_start;
*/
}
/* This needs to come before *(.text*) */
.efi_runtime : { ------------------------------------------------------------ (6)
/*
*(6)在定义了efi运行时相关支持时才会出现使用的段,一般不用关心;
*/
__efi_runtime_start = .;
*(.text.efi_runtime*)
*(.rodata.efi_runtime*)
*(.data.efi_runtime*)
__efi_runtime_stop = .;
}
.text_rest : ---------------------------------------------------------------- (7)
/*
*(7)除了start.o,其他的所有文本段将会链接到此段中;
*/
{
*(.text*)
}
#ifdef CONFIG_ARMV8_PSCI -------------------------------------------------------- (8)
/*
*(8)同(2),是PSCI相关功能的支持,一般不会使用;
*/
.__secure_start :
#ifndef CONFIG_ARMV8_SECURE_BASE
ALIGN(CONSTANT(COMMONPAGESIZE))
#endif
{
KEEP(*(.__secure_start))
}
#ifndef CONFIG_ARMV8_SECURE_BASE
#define CONFIG_ARMV8_SECURE_BASE
#define __ARMV8_PSCI_STACK_IN_RAM
#endif
.secure_text CONFIG_ARMV8_SECURE_BASE :
AT(ADDR(.__secure_start) + SIZEOF(.__secure_start))
{
*(._secure.text)
. = ALIGN(8);
__secure_svc_tbl_start = .;
KEEP(*(._secure_svc_tbl_entries))
__secure_svc_tbl_end = .;
}
.secure_data : AT(LOADADDR(.secure_text) + SIZEOF(.secure_text))
{
*(._secure.data)
}
.secure_stack ALIGN(ADDR(.secure_data) + SIZEOF(.secure_data),
CONSTANT(COMMONPAGESIZE)) (NOLOAD) :
#ifdef __ARMV8_PSCI_STACK_IN_RAM
AT(ADDR(.secure_stack))
#else
AT(LOADADDR(.secure_data) + SIZEOF(.secure_data))
#endif
{
KEEP(*(.__secure_stack_start))
. = . + CONFIG_ARMV8_PSCI_NR_CPUS * ARM_PSCI_STACK_SIZE;
. = ALIGN(CONSTANT(COMMONPAGESIZE));
KEEP(*(.__secure_stack_end))
}
#ifndef __ARMV8_PSCI_STACK_IN_RAM
. = LOADADDR(.secure_stack);
#endif
.__secure_end : AT(ADDR(.__secure_end)) {
KEEP(*(.__secure_end))
LONG(0x1d1071c); /* Must output something to reset LMA */
}
#endif
. = ALIGN(8);
.rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) } ------------------- (9)
/*
*(9)所有仅读数据将会在这个段中对齐排序存放好;
*/
. = ALIGN(8);
.data : { -------------------------------------------------------------------- (10)
/*
*(10)所有数据段将会链接到此段中;
*/
*(.data*)
}
. = ALIGN(8);
. = .;
. = ALIGN(8);
.u_boot_list : { ------------------------------------------------------------- (11)
/*
*(11)u_boot_list段定义了系统中当前支持的所有命令和设备驱动,此段把散落在各个文件中
* 通过U_BOOT_CMD的一系列拓展宏定义的命令和U_BOOT_DRIVER的拓展宏定义的设备驱动收集到一起,
* 并按照名字排序存放,以便后续在命令行快速检索到命令并执行和检测注册的设备和设备树匹配
* probe设备驱动初始化;(设备驱动的probe只在定义了dm模块化驱动时有效)
*/
KEEP(*(SORT(.u_boot_list*)));
}
. = ALIGN(8);
.efi_runtime_rel : {
__efi_runtime_rel_start = .;
*(.rel*.efi_runtime)
*(.rel*.efi_runtime.*)
__efi_runtime_rel_stop = .;
}
. = ALIGN(8);
.image_copy_end :
{
*(.__image_copy_end)
}
. = ALIGN(8);
.rel_dyn_start : -------------------------------------------------------- (12)
/*
*(12)一般u-boot运行时是根据定义的基地址开始执行,如果加载地址和链接地址
* 不一致则会出现不能执行u-boot的问题。通过一个
* 配置CONFIG_POSITION_INDEPENDENT即可打开地址无关功能,
* 此选项会在链接u-boot时添加-PIE参数。此参数会在u-boot ELF文件中
* 生成rela*段,u-boot通过读取此段中表的相对地址值与实际运行时地址值
* 依次遍历进行修复当前所有需要重定向地址,使其可以实现地址无关运行;
* 即无论链接基地址如何定义,u-boot也可以在任意ram地址
* 运行(一般需要满足最低4K或者64K地址对齐);
*
* 注意此功能只能在sram上实现,因为此功能会在运行时修改文本段数据段中的地址,
* 如果此时运行在片上flash,则不能写flash,导致功能失效无法实现地址无关;
*/
{
*(.__rel_dyn_start)
}
.rela.dyn : {
*(.rela*)
}
.rel_dyn_end :
{
*(.__rel_dyn_end)
}
_end = .;
. = ALIGN(8);
.bss_start : { -------------------------------------------------------- (13)
/*
*(13)众所周知的bbs段;
*/
KEEP(*(.__bss_start));
}
.bss : {
*(.bss*)
. = ALIGN(8);
}
.bss_end : {
KEEP(*(.__bss_end));
}
/DISCARD/ : { *(.dynsym) } -------------------------------------------- (14)
/*
*(14)一些在链接时无用需要丢弃的段;
*/
/DISCARD/ : { *(.dynstr*) }
/DISCARD/ : { *(.dynamic*) }
/DISCARD/ : { *(.plt*) }
/DISCARD/ : { *(.interp*) }
/DISCARD/ : { *(.gnu*) }
#ifdef CONFIG_LINUX_KERNEL_IMAGE_HEADER ----------------------------------- (15)
/*
*(15)在efi加载时会很有用,主要在u-boot的二进制头部添加了一些头部信息,
* 包括大小端,数据段文本段大小等,以便于efi相关的加载器读取信息,
* 此头部信息来自于Linux arm64的Image的头部信息;该头部也不属于u-boot的
* 一部分只是被附加上去的;
*/
#include "linux-kernel-image-header-vars.h"
#endif
}
2、u-boot-spl.lds
/* SPDX-License-Identifier: GPL-2.0+ */
/*
* (C) Copyright 2013
* David Feng <fenghua@phytium.com.cn>
*
* (C) Copyright 2002
* Gary Jennejohn, DENX Software Engineering, <garyj@denx.de>
*
* (C) Copyright 2010
* Texas Instruments, <www.ti.com>
* Aneesh V <aneesh@ti.com>
*/
MEMORY { .sram : ORIGIN = IMAGE_TEXT_BASE, ---------------------------------------- (1)
/*
*(1)\>XXX 的形式可以将指定段放入XXX规定的内存中;一般u-boot-spl只有
* 很小的可运行内存块,所以spl中会舍去大量不需要用的段只保留关键的
* 文本段数据段等,并且通过>.sram的形式将不在ddr初始化前用到的段定义到sdram中,
* 后续只需在完成ddr初始化后将这些段搬运到ddr中即可,而不需要额外的
* 地址修复逻辑,如下:有一个sram 0x18000-0x19000,
* 一个sdram 0x80000000 - 0x90000000,
* 那么通过>.sram方式则map文件可能如下:
* 0x18000 stext
* ...
* 0x18100 sdata
* ...
* 0x80000000 sbss
* ...
*/
LENGTH = IMAGE_MAX_SIZE }
MEMORY { .sdram : ORIGIN = CONFIG_SPL_BSS_START_ADDR,
LENGTH = CONFIG_SPL_BSS_MAX_SIZE }
OUTPUT_FORMAT("elf64-littleaarch64", "elf64-littleaarch64", "elf64-littleaarch64")
OUTPUT_ARCH(aarch64)
ENTRY(_start) -------------------------------------------------------------------- (2)
/*
*(2)同u-boot.lds一致,共用一套逻辑入口_start;
*/
SECTIONS
{
.text : {
. = ALIGN(8);
*(.__image_copy_start) -------------------------------------------------- (3)
/*
*(3)同样的,如果spl需要重定向则会使用此段定义,大多数情况下spl中会用上重定向;
*/
CPUDIR/start.o (.text*)
*(.text*)
} >.sram
.rodata : {
. = ALIGN(8);
*(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*)))
} >.sram
.data : {
. = ALIGN(8);
*(.data*)
} >.sram
#ifdef CONFIG_SPL_RECOVER_DATA_SECTION ---------------------------------------- (4)
/*
*(4)SPL_RECOVER_DATA_SECTION段用于保存数据段数据,
* 一些board在初始化时修改data段数据,并在后续某个阶段
* 从此段中恢复data的原始数据;
*/
.data_save : {
*(.__data_save_start)
. = SIZEOF(.data);
*(.__data_save_end)
} >.sram
#endif
.u_boot_list : {
. = ALIGN(8);
KEEP(*(SORT(.u_boot_list*)));
} >.sram
.image_copy_end : {
. = ALIGN(8);
*(.__image_copy_end)
} >.sram
.end : {
. = ALIGN(8);
*(.__end)
} >.sram
_image_binary_end = .;
.bss_start (NOLOAD) : {
. = ALIGN(8);
KEEP(*(.__bss_start));
} >.sdram -------------------------------------------------------------- (5)
/*
*(5)将bss段数据定义到>.sdram中,即可在初始化ddr后直接对此段地址清零
* 即可使用全局未初始化变量,并且不会带来副作用。
*/
.bss (NOLOAD) : {
*(.bss*)
. = ALIGN(8);
} >.sdram
.bss_end (NOLOAD) : {
KEEP(*(.__bss_end));
} >.sdram
/DISCARD/ : { *(.rela*) }
/DISCARD/ : { *(.dynsym) }
/DISCARD/ : { *(.dynstr*) }
/DISCARD/ : { *(.dynamic*) }
/DISCARD/ : { *(.plt*) }
/DISCARD/ : { *(.interp*) }
/DISCARD/ : { *(.gnu*) }
}
从上述的链接脚本可以看出,armv8的u-boot的启动是从arch/arm/cpu/armv8/start.S中的_start开始的
六、_start开始
从_start开始,u-boot会根据board定义做一些平台化相关的初始化工作或者是保存一些重要寄存器信息
/*************************************************************************
*
* Startup Code (reset vector)
*
*************************************************************************/
.globl _start
_start: ------------------------------------------------------------------------ (1)
#if defined(CONFIG_LINUX_KERNEL_IMAGE_HEADER) ---------------------------------- (2)
#include <asm/boot0-linux-kernel-header.h>
#elif defined(CONFIG_ENABLE_ARM_SOC_BOOT0_HOOK) -------------------------------- (3)
/*
* Various SoCs need something special and SoC-specific up front in
* order to boot, allow them to set that in their boot0.h file and then
* use it here.
*/
#include <asm/arch/boot0.h>
#else
b reset ----------------------------------------------------------------- (4)
#endif
.align 3
.globl _TEXT_BASE ------------------------------------------------------------ (5)
_TEXT_BASE:
.quad CONFIG_SYS_TEXT_BASE
/*
* These are defined in the linker script.
*/
.globl _end_ofs -------------------------------------------------------------- (5)
_end_ofs:
.quad _end - _start
.globl _bss_start_ofs
_bss_start_ofs:
.quad __bss_start - _start
.globl _bss_end_ofs
_bss_end_ofs:
.quad __bss_end - _start
reset:
/* Allow the board to save important registers */
b save_boot_params ----------------------------------------------------- (6)
.globl save_boot_params_ret
save_boot_params_ret:
... /* 此处省略无关代码,待分析到时再展开代码 */
...
WEAK(save_boot_params)
b save_boot_params_ret /* back to my caller */
ENDPROC(save_boot_params)
#endif
1、两种配置情况
(1)一种就是定义了LINUX_KERNEL_IMAGE_HEADER,boot0-linux-kernel-header.h
展开部分后如下:
.macro le64sym, sym
.long \sym\()_lo32
.long \sym\()_hi32
.endm
.globl _start
_start:
/*
* DO NOT MODIFY. Image header expected by Linux boot-loaders.
*/
b reset /* branch to kernel start, magic */
.long 0 /* reserved */
le64sym _kernel_offset_le /* Image load offset from start of RAM, little-endian */
le64sym _kernel_size_le /* Effective size of kernel image, little-endian */
le64sym _kernel_flags_le /* Informative flags, little-endian */
.quad 0 /* reserved */
.quad 0 /* reserved */
.quad 0 /* reserved */
.ascii "ARM\x64" /* Magic number */
.long 0 /* reserved */
此处将与在链接脚本中定义的LINUX_KERNEL_IMAGE_HEADER对应起来,为u-boot头部添加一个类似与Linux arm64 的Image头部,首先是起始8字节,如果没有定义efi相关的功能则是一个跳转指令,跳转到reset段继续执行启动流程,其他如链接脚本中解释一致;
(2)第二种可能的配置,就是定义了ENABLE_ARM_SOC_BOOT0_HOOK配置,此处的头文件根据不同board会引用到不同头文件
2、save_boot_params
如果对应board没有上述两种需求,那么_start段则是一条最简单的跳转指令b reset
跳转到reset处继续启动流程初始化;
在_start到reset之间,有一个.align 3
用于8字节对齐,因为可能在读取常量地址之前各自平台做了自己代码逻辑导致当前地址并不是8字节对齐的,这里不管是否对齐都强制对齐了一下。
save_boot_params用于保存一些board相关的重要寄存器,此处定义为了一个弱函数,为直接跳转回save_boot_params_ret继续往下执行,如果某些board需要保存寄存器参数则可以在自己的lowlevel.S文件中实现此函数。
3、save_boot_params_ret
save_boot_params_ret:
#if CONFIG_POSITION_INDEPENDENT
/* Verify that we're 4K aligned. */
adr x0, _start
ands x0, x0, #0xfff --------------------------------------------------------- (1)
b.eq 1f
0:
/*
* FATAL, can't continue.
* U-Boot needs to be loaded at a 4K aligned address.
*
* We use ADRP and ADD to load some symbol addresses during startup.
* The ADD uses an absolute (non pc-relative) lo12 relocation
* thus requiring 4K alignment.
*/
wfi
b 0b
1:
/*
* Fix .rela.dyn relocations. This allows U-Boot to be loaded to and
* executed at a different address than it was linked at.
*/
pie_fixup:
adr x0, _start /* x0 <- Runtime value of _start */
ldr x1, _TEXT_BASE /* x1 <- Linked value of _start */
subs x9, x0, x1 /* x9 <- Run-vs-link offset */
beq pie_fixup_done ------------------------------------------------------------- (2)
adrp x2, __rel_dyn_start /* x2 <- Runtime &__rel_dyn_start */ ----------- (3)
add x2, x2, #:lo12:__rel_dyn_start
adrp x3, __rel_dyn_end /* x3 <- Runtime &__rel_dyn_end */
add x3, x3, #:lo12:__rel_dyn_end
pie_fix_loop: ---------------------------------------------------------------------- (4)
ldp x0, x1, [x2], #16 /* (x0, x1) <- (Link location, fixup) */
ldr x4, [x2], #8 /* x4 <- addend */
cmp w1, #1027 /* relative fixup? */
bne pie_skip_reloc
/* relative fix: store addend plus offset at dest location */
add x0, x0, x9
add x4, x4, x9
str x4, [x0]
pie_skip_reloc:
cmp x2, x3
b.lo pie_fix_loop
pie_fixup_done:
#endif
此部分的功能主要是在定义POSITION_INDEPENDENT后,进行地址无关的相对地址修复,以此保证后续在跳入c语言部分时可正常执行,一般不定义此配置则是继续往下执行boot流程。
如在链接脚本中说的,地址无关功能最基本需要保证加载地址是4K对齐的,经过一些测试发现某些情况需要64K对齐。此处使用adr指令获取_start的运行时地址并检验是否是4K对齐,如果是则继续往下执行,如果不是则调用wfi指令挂死在此处。wfi为等待中断指令,只有在接收到中断事件是才会唤醒cpu继续往下执行。
通过对运行时地址和加载地址相减得到一个地址偏移值,如果偏移值等于0,说明加载地址和运行时地址是一致的不需要进行地址修复,则直接跳pie_fixup_done继续下面流程初始化,否则就进入地址修复逻辑。
为什么不使用adr指令而是adrp指令,adr指令是一个小范围读相对pc指针地址内存的指令,可以使用adr说明读取的地址一定是离pc指针很近的位置,而当读取__rel_dyn_start这种并不能确定实际地址在哪里的地址时则只能使用大范围读地址指令的adrp指令,不过adrp指令是以页为单位进行读取的,所以add x2, x2, #:lo12:__rel_dyn_start
的作用就是将__rel_dyn_start地址的页内偏移读取出来并与页对齐的运行地址相加得到实际的运行地址。(此操作指令在Linux上被封装为adr_l
指令)
4、elf动态库重定位
typedef struct {
Elf64_Addr r_offset;
Elf64_Xword r_info;
Elf64_Sxword r_addend;
} Elf64_Rela;
当对动态库进行地址重定位时首先会去查找rela.dyn
段中的信息,此段中每一组信息就是上面结构体定义的信息,对于64为系统的elf则是24字节为一个表,r_offset保存需要重定位作用的地址位置,r_info描述此表重定位类型此类型特定于处理器,如arm64位则是:
/* AArch64 relocs */
#define R_AARCH64_NONE 0 /* No relocation */
#define R_AARCH64_RELATIVE 1027 /* Adjust by program base */
r_addend是一个常量加数,用于计算存储在可重定位字段中的值。
对表进行重定位有以下公式:重定位为值 = *(r_offset + 实际偏移值) = 实际偏移值 + r_addend
。
根据此公式则可以重定向每一个需要重定向的地址值。
x0保存r_offset值,x1保存r_info值,x4保存r_addend,并通过x1与R_AARCH64_RELATIVE比较看是否属于aarch64相对地址类型,如果不是则不是需要重定向的一组表则跳到pie_skip_reloc判断是否完成所有rela.dyn段重定向,如果是则继续往下执行启动流程初始化,如果不是则跳回pie_fix_loop继续下一组表的重定向。当判断属于aarch64相对地址类型时进入重定向逻辑,首先需要知道x9是运行地址减去链接地址得到的偏移地址,那么实际运行地址也等于链接地址加上x9,所以add x0, x0, x9
,add x4, x4, x9
,x0是需要修复的重定位运行地址,x4是实际运行时需要附加的常量值,根据上面的公式,将x4这个由附加常量值加偏移值得到的运行时附加常量值写入到x0这个运行时重定向地址即可完成整个重定向修复功能。
5、进入_main之前
在完成地址无关fixup后,u-boot开始对一些系统寄存器进行初始化
pie_fixup_done:
#endif
#ifdef CONFIG_SYS_RESET_SCTRL
bl reset_sctrl --------------------------------------------------------------------- (1)
#endif
#if defined(CONFIG_ARMV8_SPL_EXCEPTION_VECTORS) || !defined(CONFIG_SPL_BUILD) ---------- (2)
.macro set_vbar, regname, reg
msr \regname, \reg
.endm
adr x0, vectors
#else
.macro set_vbar, regname, reg
.endm
#endif
/*
* Could be EL3/EL2/EL1, Initial State:
* Little Endian, MMU Disabled, i/dCache Disabled
*/
switch_el x1, 3f, 2f, 1f ---------------------------------------------------------- (3)
3: set_vbar vbar_el3, x0
mrs x0, scr_el3
orr x0, x0, #0xf /* SCR_EL3.NS|IRQ|FIQ|EA */
msr scr_el3, x0
msr cptr_el3, xzr /* Enable FP/SIMD */
b 0f
2: mrs x1, hcr_el2
tbnz x1, #34, 1f /* HCR_EL2.E2H */
set_vbar vbar_el2, x0
mov x0, #0x33ff
msr cptr_el2, x0 /* Enable FP/SIMD */
b 0f
1: set_vbar vbar_el1, x0
mov x0, #3 << 20
msr cpacr_el1, x0 /* Enable FP/SIMD */
0:
#ifdef COUNTER_FREQUENCY -------------------------------------------------------------- (4)
branch_if_not_highest_el x0, 4f
ldr x0, =COUNTER_FREQUENCY
msr cntfrq_el0, x0 /* Initialize CNTFRQ */
#endif
4: isb ------------------------------------------------------------------------------- (5)
...
...
#ifdef CONFIG_SYS_RESET_SCTRL
reset_sctrl:
switch_el x1, 3f, 2f, 1f
3:
mrs x0, sctlr_el3
b 0f
2:
mrs x0, sctlr_el2
b 0f
1:
mrs x0, sctlr_el1
0:
ldr x1, =0xfdfffffa
and x0, x0, x1
switch_el x1, 6f, 5f, 4f
6:
msr sctlr_el3, x0
b 7f
5:
msr sctlr_el2, x0
b 7f
4:
msr sctlr_el1, x0
7:
dsb sy
isb
b __asm_invalidate_tlb_all ----------------------------------------------------- (6)
ret
#endif
使用CONFIG_SYS_RESET_SCTRL来决定是否重置系统控制寄存器,包括保证处理器处于小端,关闭data cache,关闭mmu。其中switch_el
是一个宏,用于读取当前所处的异常级别,根据所处异常级别调用对应的系统控制寄存器。
定义设置异常向量表的宏,将异常向量表的地址写入/reg设置的系统寄存器即可完成异常向量表的设置,这里u-boot是需要设置异常向量表的,而spl默认是不需要设置异常向量表的,毕竟spl只是一个加载器只会运行一次,不过当定义了CONFIG_ARMV8_SPL_EXCEPTION_VECTORS时可以为spl也设置一个异常向量表。
同样的使用switch_el
来跳转到对应级别的路径上去执行,在进行系统寄存器设置时,因为在这之前已经由SYS_RESET_SCTRL或者board自己保证处理器处于小端,mmu关,i-cache和d-cache处于关闭状态了,所以这里直接进行对应级别系统寄存器设置,首先是跳转到对应表设置对应级别的异常向量表。接着会有如下三种情况:
(1)当处于EL3时,会设置安全配置系统寄存器(scr_el3),会将低四位bit设置为0xf,表示设置处理器处于非安全模式,任何级别的物理irq中断,物理fiq,异常abort中断,异常SError中断都将被路由到el3级别。后续这些设置将在启动Linux时被修改,这些设置仅用于在u-boot阶段。接着将cptr_el3清零,使用xzr是可以快速操作寄存器为零。这里保证任何级别下访问SIMD和floating-point指令不会导致触发异常陷入el3。
(2)当处于EL2时,首先根据HCR_EL2.E2H判断系统是一个虚拟机管理器还是主机系统,当E2H = 0时,表示系统处于主机系统只需要做el3一样的操作配置SIMD和FP指令不会陷入el2即可。
(3)当系统处于EL1时,则什么也不需要操作只需要配置SIMD和FP指令不会陷入el1。
u-boot在启动时系统的时钟频率不一定配置了,所以当在include/configs/xxxxxx.h中定义了COUNTER_FREQUENCY的频率值时,说明需要在此处配置系统时钟,所以根据宏 branch_if_not_highest_el判断当系统不处于EL3时则需要设置系统的时钟工作频率cntfrq_el0,后续Linux或者u-boot根据读出的这个值计算出系统每纳秒的滴答数从而供软件获取时间流逝值。
isb指令用于确保上述操作指令被正确真正的执行了,属于同步指令的一种。
在进行系统控制器复位时,dsb sy
,isb
,__asm_invalidate_tlb_all
三个操作在这里的意义是,因为对处理器的小端,mmu,d-cache进行了复位,所以这里必须通过dsb和isb确保数据和指令全部执行和写入,这里进行了mmu和cache关闭操作,那么如果有缓存的tlb在这个时候这些缓存的tlb数据就是无效的,这里对可能缓存的tlb进行全部无效化,确保后续任何可能的mmu开启操作不会使用到这些无用的tlb条目而导致系统异常。
6、进入_main之前的第二段
4: isb
/*
* Enable SMPEN bit for coherency.
* This register is not architectural but at the moment
* this bit should be set for A53/A57/A72.
*/
#ifdef CONFIG_ARMV8_SET_SMPEN -------------------------------------------------------- (1)
switch_el x1, 3f, 1f, 1f
3:
mrs x0, S3_1_c15_c2_1 /* cpuectlr_el1 */
orr x0, x0, #0x40
msr S3_1_c15_c2_1, x0
isb
1:
#endif
/* Apply ARM core specific erratas */
bl apply_core_errata ------------------------------------------------------------ (2)
/*
* Cache/BPB/TLB Invalidate
* i-cache is invalidated before enabled in icache_enable()
* tlb is invalidated before mmu is enabled in dcache_enable()
* d-cache is invalidated before enabled in dcache_enable()
*/
/* Processor specific initialization */
bl lowlevel_init ---------------------------------------------------------------- (3)
...
...
WEAK(apply_core_errata)
...
...
WEAK(lowlevel_init)
mov x29, lr /* Save LR */
#if defined(CONFIG_GICV2) || defined(CONFIG_GICV3) ----------------------------------- (4)
branch_if_slave x0, 1f
ldr x0, =GICD_BASE
bl gic_init_secure
1:
#if defined(CONFIG_GICV3)
ldr x0, =GICR_BASE
bl gic_init_secure_percpu
#elif defined(CONFIG_GICV2)
ldr x0, =GICD_BASE
ldr x1, =GICC_BASE
bl gic_init_secure_percpu
#endif
#endif
#ifdef CONFIG_ARMV8_MULTIENTRY ------------------------------------------------------ (5)
branch_if_master x0, x1, 2f
/*
* Slave should wait for master clearing spin table.
* This sync prevent salves observing incorrect
* value of spin table and jumping to wrong place.
*/
#if defined(CONFIG_GICV2) || defined(CONFIG_GICV3)
#ifdef CONFIG_GICV2
ldr x0, =GICC_BASE
#endif
bl gic_wait_for_interrupt
#endif
/*
* All slaves will enter EL2 and optionally EL1.
*/
adr x4, lowlevel_in_el2
ldr x5, =ES_TO_AARCH64
bl armv8_switch_to_el2
lowlevel_in_el2:
#ifdef CONFIG_ARMV8_SWITCH_TO_EL1
adr x4, lowlevel_in_el1
ldr x5, =ES_TO_AARCH64
bl armv8_switch_to_el1
lowlevel_in_el1:
#endif
#endif /* CONFIG_ARMV8_MULTIENTRY */
2:
mov lr, x29 /* Restore LR */
ret
ENDPROC(lowlevel_init)
有一些架构定义系统寄存器是不能被编译器识别的,如果要访问则只能直接通过如下定义的形式被ARM汇编器识别并编译成二进制:S3_<op1><Cn><Cm>_<op2> 。这里使用S3_1_C15_C2_0,则不需要将编码交给汇编程序,如果直接使用对应的CPUACTLR_EL1则必须把它的编码交给汇编程序。这里是当定义了CONFIG_ARMV8_SET_SMPEN并处于EL3级别时设置cpuectlr_el1的SMPEN位用于开启多核之间的数据一致性功能,当然这个功能是因为设计的问题导致只是一些a系列处理器需要设置,如a53,a57,a72。
因为一些基于armv8架构设计的处理器本身会存在一些bug,所以这里对特定处理器进行勘误设置,相当于打补丁的意思,感兴趣的可以去看看每个a系列有哪些勘误需要被设置。
低平台初始化,这里可以在汇编阶段对对应平台board进行初始化,lowlevel_init是一个弱符号,可以由厂商自己实现,这里分析armv8的标准lowlevel_init进行的初始化。
首先是对GIC的一些初始化,同样的需要根据使用的gic版本在Kconfig中选中CONFIG_GICV2或者CONFIG_GICV3,当定义了这两个宏其中一个时就会对gic进行相应初始化。
(1)当定义的是GICV2时,则需要在include/configs/xxxxx.h中定义GICD_BASE和GICC_BASE,分别说明GIC的分发器基地址和cpu接口寄存器基地址。
(2)当定义的是GICV3时,则需要在include/configs/xxxxx.h中定义GICD_BASE,说明GIC的主分发器基地址,因为cpu接口在使用gicv3时可通过armv8拓展的系统寄存器接口访问,所以这里不用定义cpu接口的基地址,gicv3使用系统寄存器访问cpu接口的原因,主要是因为cpu对gic cpu接口寄存器的访问是频繁的,为了少一些对系统总线的访问,直接在v3中使用系统寄存器访问,大大提高了cpu接口的读写速度,提高整体性能。
当是boot cpu执行时则会调用gic_init_secure对应安全组等相关的初始化,具体代码在arch/arm/lib/gic_64.S中,这里不展开只说明具体做了哪些事情:
(1)如果是gicv3则主要是对分发器的初始化:激活group0,激活非安全的group1,激活安全的group1,激活安全和非安全的亲和性路由(绑定中断到某些cpu功能)。读取支持的最大spi中断数量,并配置。
(2)如果是gicv2则首先对分发器进行初始化,激活group0和group1,和读取spi的支持数并配置。接着初始化cpu接口寄存器,激活group0和group1,并配置允许非安全模式下对GICC_PMR的访问,GICC_PMR是中断优先级的配置,在linux上没有对优先级有过多使用,只有一次性配置。
当是从核cpu执行到这里时则会根据branch_if_slave跳转到1标号处进行percpu的gic初始化,branch_if_slave的实现是读取cpu各自的标识符寄存器mpidr_el1获取自己的簇号进行区分的,细节可以查看此系统寄存器的描述。
同样的对percpu的gic初始化是gic_init_secure_percpu,意思就是每个cpu都要进行相应的设置,比如gicv3会对每个cpu的标识进行识别,以便精确ppi中断的分发,所以会将每个cpu的mpidr_el1值写入对应的每个cpu的重分发器里,所以称为percpu初始化,这也是gicv3能支持到128个cpu的原因,而gicv2只能支持8个cpu核心,具体实现可看arch/arm/lib/gic_64.S。
u-boot虽然是运行单个cpu的程序,但也允许从核cpu进入u-boot,这里典型的例子就是使用SPIN_TABLE方式启动从核在u-boot中自选等待进入linux。当要使用spin_table时则必须开启CONFIG_ARMV8_MULTIENTRY 选项,用于让从核进入u-boot进行基本初始化。首先如果是主核自然不用走从核流程则直接在这里返回完成lowlevel_init的调用。如果是从核并且定义了gicv2或者gicv3则首先是当从核进入u-boot后,进入gic_wait_for_interrupt使用wfi休眠,等待主核的事件唤醒,也就是smp_kick_all_cpus操作,让从核cpu可以继续往下执行。bl armv8_switch_to_el2
因为u-boot处于el3级别时,当启动linux时会将异常级别降低到el2或者el1来启动linux,根据具体设置来切换,而处于el3时则会将异常级别切换到el2,所以这里的操作是与主核一致,从核要进入linux,首先就是要将自己的异常级别从el3切换到el2。是否真实的能切到el2还要根据自己当前的级别,如果已经是el1了自然无法切换到el2。
当然如果定义了CONFIG_ARMV8_SWITCH_TO_EL1,说明还得切换从核到el1,这里将从核切换到el1级别去。
6、进入_main之前的第三段
#if defined(CONFIG_ARMV8_SPIN_TABLE) && !defined(CONFIG_SPL_BUILD) --------------------- (1)
branch_if_master x0, x1, master_cpu
b spin_table_secondary_jump
/* never return */
#elif defined(CONFIG_ARMV8_MULTIENTRY)
branch_if_master x0, x1, master_cpu
/*
* Slave CPUs
*/
slave_cpu: ----------------------------------------------------------------------------- (2)
wfe
ldr x1, =CPU_RELEASE_ADDR
ldr x0, [x1]
cbz x0, slave_cpu
br x0 /* branch to the given address */
#endif /* CONFIG_ARMV8_MULTIENTRY */
master_cpu:
bl _main
... /* arch/arm/cpu/armv8/spin_table_v8.S */
ENTRY(spin_table_secondary_jump) ------------------------------------------------------ (3)
.globl spin_table_reserve_begin
spin_table_reserve_begin:
0: wfe
ldr x0, spin_table_cpu_release_addr
cbz x0, 0b
br x0
.globl spin_table_cpu_release_addr
.align 3
spin_table_cpu_release_addr:
.quad 0
.globl spin_table_reserve_end
spin_table_reserve_end:
ENDPROC(spin_table_secondary_jump)
首先是当定义了CONFIG_ARMV8_SPIN_TABLE启动从核时并且不处于SPL中,spl是不需要启动从核的。当是主核运行时直接跳转到_main离开系统寄存器初始化,开始下一段board_f等的u-boot初始化流程,如果是从核则会跳转到spin_table_secondary_jump,在这里,从核cpu将会开始自旋,使用wfe指令休眠,并在接收到中断或者sev事件时醒来并读取spin_table_cpu_release_addr地址里的值,如果是0说明并不是引导进入linux的事件发生则继续调用wfe休眠等待唤醒,如果spin_table_cpu_release_addr地址里不是0说明这时候已经由linux唤醒了,从核需要进入linux开始下一段旅程,则直接跳转到指定的地址里去继续执行。spin_table_cpu_release_addr地址将会在u-boot加载linux的设备树时直接写入到linux使用的设备树的对应节点中去,以便不需要由人为指定地址,linux和u-boot即可完成从核启动的交接,这就是spin_table的工作原理。
当不是spin_table启动从核时,说明这是由用户定义需要在u-boot或者其他地方使用从核,所以直接通过CPU_RELEASE_ADDR宏定义了一个跳转地址的存储地址,在用户任何时候需要从核执行时往CPU_RELEASE_ADDR地址写入对应要跳转的地址后开始执行从核。
在用户任何时候需要从核执行时往CPU_RELEASE_ADDR地址写入对应要跳转的地址后开始执行从核。