本篇开始进入uboot启动的分析,先分析下lds链接文件,然后就是从汇编语言开始进行启动分析。
1. 启动的两个过程简介
UBoot其启动过程主要可以分为两个部分,Stage1和Stage2 。其中Stage1是用汇编语言实现的,主要完成硬件资源的初始化。而Stage2则是用C语言实现。主要完成内核程序的调用。这两个部分的主要执行流程如下:
stage1包含以下步骤:
- 硬件设备初始化
- 为加载stage2准备RAM空间
- 拷贝stage2的代码到RAM空间
- 设置好堆栈
- 跳转到stage2的C语言入口点
stage2一般包括以下步骤:
- 初始化本阶段要使用的硬件设备
- 检测系统内存映射
- 将kernel映射和根文件系统映射从Flash读到RAM空间中
- 为内核设置启动参数
- 调用内核
2. u-boot的链接脚本
启动的入口怎么找?
这个肯定要从链接脚本里面找ENTRY(XXX),在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。
在进行源码分析之前,首先看看u-boot的链接脚本,通过链接脚本可以从整体了解一个u-boot的组成,并且可以在启动分析中知道某些逻辑是在完成什么工作。
2.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
}
_start 在文件 arch/arm/lib/vectors.S 中有定义
.globl _start
.section ".vectors", "ax"
_start:
#ifdef CFG_SYS_DV_NOR_BOOT_CFG
.word CFG_SYS_DV_NOR_BOOT_CFG
#endif
ARM_VECTORS
#endif /* !defined(CONFIG_ENABLE_ARM_SOC_BOOT0_HOOK) */
#if !CONFIG_IS_ENABLED(SYS_NO_VECTOR_TABLE)
.globl _reset
.globl _undefined_instruction //未定义的指令异常 0x4
.globl _software_interrupt // 软件中断异常 0x8
.globl _prefetch_abort
.globl _data_abort //数据异常
.globl _not_used
.globl _irq //中断IRQ异常
.globl _fiq //快速中断FIQ异常
arch/arm/cpu/armv8/start.S中定义了reset
reset:
/* Allow the board to save important registers */
b save_boot_params
.globl save_boot_params_ret
save_boot_params_ret:
armv8的异常向量表格式如下:
向量表定义在arch/arm/cpu/armv8/exceptions.S中
2.2 u-boot-spl.lds
此链接脚本是标准的spl链接脚本,还包含了u_boot_list段,如果对应自己board不需要命令行或者模块化驱动设备,只作为一个加载器则可以自定义更简略的链接脚本。
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开始的,并在后续初始化中调用了很多链接脚本中定义的地址符号表。
3. 第一阶段
3.1 armv8/start.S
_start 开始的是中断向量表,在arch/arm/lib/vectors.S中定义。之后是跳转到reset开始执行。 (1)save_boot_params保存上一级镜像传入的参数,该函数由平台自行定义
(2)若支持pie则检查代码段是否为4k对齐(因为由于指令集中操作数长度的限制,adr等类型指令的寻址范围是需要4k对齐的)
(3)pie_fixup为pie重定位全局地址相关的.rela.dyn段内容
(4)reset_sctrl根据配置确定是否重设sctlr寄存器
(5)为uboot设置异常向量表。spl和uboot异常向量表设置有以下不同:
(6)若配置了COUNTER_FREQUENCY选项,则根据当前正在运行的异常等级,确定是否要设置cpu的system counter的频率。由于system counter的频率是所有异常等级共享的,为了确保该频率不被随意修改,因此约定只有运行于最高异常等级时才允许修改该寄存器
(7)若设置了配置选项CONFIG_ARMV8_SET_SMPEN,则设置S3_1_c15_c2_1以使能cpu之间的数据一致性
(8)apply_core_errata用于处理cpu的errata
(9)lowlevel_init流程可参考spl启动分析
(10)secondary cpu处理流程
(11)_main的定义位于arch/arm/lib/crt0_64.S
.globl _start
_start:
#if defined(CONFIG_LINUX_KERNEL_IMAGE_HEADER)
#include <asm/boot0-linux-kernel-header.h>
#elif defined(CONFIG_ENABLE_ARM_SOC_BOOT0_HOOK)
/*
* 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
#endif
reset如下:
reset:
/* Allow the board to save important registers */
b save_boot_params
.globl save_boot_params_ret
save_boot_params_ret:
save_boot_params 函数调用了save_boot_params_ret 函数 先进行校验_start地址是否4K对齐,然后继续执行pie_fixup
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
adrp x2, __rel_dyn_start /* x2 <- Runtime &__rel_dyn_start */
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:
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:
/* Apply ARM core specific erratas */
bl apply_core_errata
/*
* 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
#if defined(CONFIG_ARMV8_SPIN_TABLE) && !defined(CONFIG_SPL_BUILD)
branch_if_master x0, master_cpu
b spin_table_secondary_jump
/* never return */
#elif defined(CONFIG_ARMV8_MULTIENTRY)
branch_if_master x0, master_cpu
/*
* Slave CPUs
*/
slave_cpu:
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:
msr SPSel, #1 /* make sure we use SP_ELx */
bl _main
lowlevel_init的定义:
WEAK(lowlevel_init)
mov x29, lr /* Save LR */
#if defined(CONFIG_GICV2) || defined(CONFIG_GICV3)
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
(1)该函数首先将链接寄存器的值lr保存到x29中,然后根据中断控制器的型号分别处理。假设我们系统中的中断控制器为GICV3,则会执行第二步。
(2)branch_if_slave 定义在rch/arm/include/asm/macro.h中,代码如下。它会读取控制寄存器mpidr_el1的值,然后测试它的相应字段,以确定其是否slave。mpidr_el1寄存器用于在多处理器系统中标识不同的处理器,此处就是通过对该值的判断来确定当前处理器是否为master的。为了介绍方便,后面我们都假设当前cpu为master。
(3)若当前cpu为master,则先将GICD_BASE的基地址加载到x0寄存器中
(4)跳转到gic_init_secure宏中, 该宏的定义位于arm/lib/gic_64.S中,它的作用是为了初始化中断控制器gic。我们知道arm处理器的外设中断是通过irq和fiq中断线触发的,实际上在arm和外设之间还有一个处理中断的设备GIC,外设中断线连接到GIC上,当其中断线触发中断时GIC就会接收到中断事件,然后它根据配置情况将该中断分发给cpu,此时cpu才进入irq或fiq异常处理中断。
(5)和(6)设置GIC对每个cpu相关的配置
(9)arm的多处理器相关的设置,主要是slave cpu和master cpu同步相关的操作
(10)恢复前面保存的lr值,并返回
3.2 _main
arch/arm/lib/crt0_64.S中定义。
在进入c语言之前,我们需要为其准备好运行环境,以及做好内存规划,这其中除了栈和堆内存之外,还需要为gd结构体分配内存空间。gd是uboot中的一个global_data类型全局变量,该变量包含了很多全局相关的参数,为各模块之间参数的传递和共享提供了方便。由于该变量在跳转到c流程之前就需要准备好,此时堆管理器尚未被初始化,所以其内存需要通过手工管理方式分配。以下为uboot内存规划相关代码:
/*
* Set up initial C runtime environment and call board_init_f(0).
*/
#if defined(CONFIG_TPL_BUILD) && defined(CONFIG_TPL_NEEDS_SEPARATE_STACK)
ldr x0, =(CONFIG_TPL_STACK)
#elif defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
ldr x0, =(CONFIG_SPL_STACK)
#elif defined(CONFIG_INIT_SP_RELATIVE)
#if CONFIG_POSITION_INDEPENDENT
adrp x0, __bss_start /* x0 <- Runtime &__bss_start */
add x0, x0, #:lo12:__bss_start
#else
adr x0, __bss_start
#endif
add x0, x0, #CONFIG_SYS_INIT_SP_BSS_OFFSET
#else
ldr x0, =(SYS_INIT_SP_ADDR) //获取uboot的初始栈地址
#endif
bic sp, x0, #0xf //为了遵循ABI规范,栈地址需要16字节对齐
mov x0, sp //读取 sp 到寄存器 x0 里面,作为下面函数的入参
bl board_init_f_alloc_reserve //gd和early malloc分配内存
mov sp, x0 //将预留后的内存地址设置为新的栈地址
/* set up gd here, outside any C code */
mov x18, x0 //将gd地址保存到x18寄存器中
bl board_init_f_init_reserve //初始化gd,和设置early malloc的堆管理器基地址
#if defined(CONFIG_DEBUG_UART) && CONFIG_IS_ENABLED(SERIAL)
bl debug_uart_init
#endif
mov x0, #0
bl board_init_f
board_init_f_alloc_reserve在common/init/board_init.c中定义
ulong board_init_f_alloc_reserve(ulong top)
{
/* Reserve early malloc arena */
#ifndef CFG_MALLOC_F_ADDR
#if CONFIG_VAL(SYS_MALLOC_F_LEN)
top -= CONFIG_VAL(SYS_MALLOC_F_LEN); //为早期堆管理器预留内存
#endif
#endif
/* LAST : reserve GD (rounded up to a multiple of 16 bytes) */
top = rounddown(top-sizeof(struct global_data), 16); //为gd预留内存
return top;
}
define CONFIG_SYS_MALLOC_F_LEN 0x2000 include/generated/autoconf.h 中定义,默认是8KB。函数的返回值是新的top值。预留后的地址如下图:
board_init_f_init_reserve的定义如下:
void board_init_f_init_reserve(ulong base)
{
struct global_data *gd_ptr;
/*
* clear GD entirely and set it up.
* Use gd_ptr, as gd may not be properly set yet.
*/
gd_ptr = (struct global_data *)base;
/* zero the area */
memset(gd_ptr, '\0', sizeof(*gd)); //初始化gd清零
/* set GD unless architecture did it already */
#if !defined(CONFIG_ARM)
arch_setup_gd(gd_ptr); //用于非arm架构的gd指针获取,armv8架构则通过前面设置的x18寄存器获取gd指针
#endif
if (CONFIG_IS_ENABLED(SYS_REPORT_STACK_F_USAGE))
board_init_f_init_stack_protection_addr(base);//用于获取该栈溢出检测的地址
/* next alloc will be higher by one GD plus 16-byte alignment */
base += roundup(sizeof(struct global_data), 16);
#if CONFIG_VAL(SYS_MALLOC_F_LEN)
/* go down one 'early malloc arena' */
gd->malloc_base = base;//设置early malloc的基地址
#endif
if (CONFIG_IS_ENABLED(SYS_REPORT_STACK_F_USAGE))
board_init_f_init_stack_protection(); //初始化栈溢出检测的canary值,该值被设置为SYS_STACK_F_CHECK_BYTE
}
board_init_f 函数,此函数定义在文件 common/board_f.c 中,主要用来初始化 DDR,定时器,完成代码拷贝等等
c_runtime_cpu_setup
ENTRY(c_runtime_cpu_setup)
#if defined(CONFIG_ARMV8_SPL_EXCEPTION_VECTORS) || !defined(CONFIG_SPL_BUILD)
/* Relocate vBAR */
adr x0, vectors
switch_el x1, 3f, 2f, 1f
3: msr vbar_el3, x0
b 0f
2: msr vbar_el2, x0
b 0f
1: msr vbar_el1, x0
0:
#endif
3.3 uboot重定位
一般的启动流程会由spl初始化ddr,然后将uboot加载到ddr中运行。但是uboot在norflash中就需要进行搬运到ddr中,从而加快运行速度。
重定位的前提:
uboot重定位依赖于位置无关代码技术,因此需要在编译和重定位时添加以下支持:
(1)编译时添加-fpie选项
(2)在链接时添加-pie选项,它使得链接器会产生.rel.dyn和.dynsym段的fixup表。
(3)链接脚本中添加.rel.dyn和.dynsym段定义,并为重定位代码访问这些段的数据提供符号信息
(4)在重定位过程中需要根据新的地址fixup .rel.dyn和.dynsym段的数据
内核需要从内存的低地址开始运行,为了防止内核三件套(kernel、dtb和ramdisk)的加载地址与uboot运行地址重叠,因此uboot的重定位地址需要被设置到内存顶端附近。同时我们还需要为一些特定模块预留一些内存空间(比如页表空间、framebuffer等),上图就是uboot规划的重定位后内存布局:
该图中橙色部分都是需要执行重定位操作的,如uboot的代码段、数据段,以及gd、设备树等,它们都是在board_init_r阶段还需要使用的。对于gd和dtb等纯数据的重定位,只需要将数据拷贝到新的地址,并将其基地址指针切换到新地址即可。但对于代码段的重定位我们还需要考虑以下问题:
(1)位置无关代码需要调整.rel.dyn和.dynsym段
(2)栈指针需要切换到新的位置
(3)重定位完成后如何完成pc的平滑切换
#if !defined(CONFIG_SPL_BUILD)
ldr x0, [x18, #GD_START_ADDR_SP] //获取新的栈指针地址
bic sp, x0, #0xf //设置新的栈
ldr x18, [x18, #GD_NEW_GD] //将新的gd地址设置到x18,以将gd切换到新的位置
adr lr, relocation_return //将重定位返回位置加载到lr中,在重定位流程中,这个地址将会被调整到新代码段的对应位置处。并在重定位完成后跳转到该地址处执行,从而完成代码从老位置到新位置的切换
#if CONFIG_POSITION_INDEPENDENT
adrp x0, _start
add x0, x0, #:lo12:_start
ldr x9, _TEXT_BASE
sub x9, x9, x0
add lr, lr, x9 //若定义了位置无关选项CONFIG_POSITION_INDEPENDENT,则计算其偏移值,并用该偏移值调整lr的值
#if defined(CONFIG_SYS_RELOC_GD_ENV_ADDR)
ldr x0, [x18, #GD_ENV_ADDR]
add x0, x0, x9
str x0, [x18, #GD_ENV_ADDR] //若定义了环境变量重定位选项CONFIG_SYS_RELOC_GD_ENV_ADDR,则将环境变量的地址调整到新的位置
#endif
#endif
ldr x9, [x18, #GD_RELOC_OFF]
add lr, lr, x9 //根据重定位偏移调整lr的位置
ldr x0, [x18, #GD_RELOCADDR]
b relocate_code //进入实际的代码重定位流程
relocate_code 在arch/arm/lib/relocate_64.S中实现
ENTRY(relocate_code)
stp x29, x30, [sp, #-32]!
mov x29, sp
str x0, [sp, #16] //构造一个栈帧,该栈帧中包含lr寄存器x30,fp寄存器x29和函数入参x0,其中x0为重定位的起始目的地址
adrp x1, __image_copy_start
add x1, x1, :lo12:__image_copy_start
subs x9, x0, x1
b.eq relocate_done//计算镜像运行地址与目的地址的偏移,若它们相等,则显然无须执行重定位,可直接跳过该流程
ldr x1, _TEXT_BASE
subs x9, x0, x1 //计算镜像链接地址与目的地址的偏移
adrp x1, __image_copy_start
add x1, x1, :lo12:__image_copy_start
adrp x2, __image_copy_end
add x2, x2, :lo12:__image_copy_end //读取镜像运行地址的起始地址和结束地址
copy_loop: //从运行地址处将镜像拷贝到重定位目的地址处
ldp x10, x11, [x1], #16
stp x10, x11, [x0], #16
cmp x1, x2
b.lo copy_loop
str x0, [sp, #24] //将重定位结束地址入栈
adrp x2, __rel_dyn_start //位置无关代码相关处理
relocate_done:
switch_el x1, 3f, 2f, 1f //根据当前执行的异常等级,跳转到对应的位置以读取sctlr寄存器的内容
bl hang
3: mrs x0, sctlr_el3
b 0f
2: mrs x0, sctlr_el2
b 0f
1: mrs x0, sctlr_el1
0: tbz w0, #2, 5f
tbz w0, #12, 4f //由于重定位后pc将会跳转到新的位置执行,因此若使能了cache,显然重定位之前已加载到cache中的指令还是老的地址,此时若直接跳转则cache中的内容是错误的。因此必须要失效掉cache中已经加载的内容
ic iallu
isb sy
4: ldp x0, x1, [sp, #16]
bl __asm_flush_dcache_range
bl __asm_flush_l3_dcache
5: ldp x29, x30, [sp],#32 //从栈帧中恢复x29和x30(lr)的内容。现在已经万事俱备,只欠东风了,我们只要通过ret命令跳转到新地址执行即可
Ret
3.4 board_init_f 函数
_main 中会调用 board_init_f 函数,board_init_f 函数主要有两个工作:
- 初始化一系列外设,比如串口、定时器,或者打印一些消息等。
- 初始化 gd 的各个成员变量,uboot 会将自己重定位到 DRAM 最后面的地址区域,也就是将自己拷贝到 DRAM 最后面的内存区域中。这么做的目的是给 Linux 腾出空间,防止 Linuxkernel 覆盖掉 uboot,将 DRAM 前面的区域完整的空出来。
在拷贝之前肯定要给 uboot 各部分分配好内存位置和大小,比如 gd 应该存放到哪个位置,malloc 内存池应该存放到哪个位置等等。这些信息都保存在 gd 的成员变量中,因此要对 gd 的这些成员变量做初始化。最终形成一个完整的内存“分配图”,在后面重定位 uboot 的时候就会用到这个内存“分配图”。
void board_init_f(ulong boot_flags)
{
gd->flags = boot_flags;
gd->have_console = 0;
if (initcall_run_list(init_sequence_f))
hang();
#if !defined(CONFIG_ARM) && !defined(CONFIG_SANDBOX) && \
!defined(CONFIG_EFI_APP) && !CONFIG_IS_ENABLED(X86_64) && \
!defined(CONFIG_ARC)
/* NOTREACHED - jump_to_copy() does not return */
hang();
#endif
}
通过函数 initcall_run_list 来运行初始化序列 init_sequence_f 里面的一些列函数,init_sequence_f 里面包含了一系列的初始化函数,init_sequence_f 也是定义在文件common/board_f.c 中
static const init_fnc_t init_sequence_f[] = {
setup_mon_len,
fdtdec_setup,
trace_early_init,
initf_malloc, //函数初始化 gd 中跟 malloc 有关的成员变量,比如 malloc_limit
log_init,
initf_bootstage, /* uses its own timer, so does not need DM */
event_init,
bloblist_init,
setup_spl_handoff,
console_record_init,
arch_fsp_init,
arch_cpu_init, /* basic arch cpu dependent setup */
mach_cpu_init, /* SoC/machine dependent CPU setup */
initf_dm,
board_early_init_f,
/* get CPU and bus clocks according to the environment variable */
get_clocks, /* get CPU and bus clocks (etc.) */
timer_init, /* 初始化定时器 */
board_postclk_init,
env_init, /* initialize environment */
init_baud_rate, /* 初始化波特率 */
serial_init, /* 初始化串口 */
console_init_f, /* stage 1 init of console */
display_options, /* 通过串口输出一些信息 */
display_text_info, /* 打印一些文本信息 */
checkcpu,
print_resetinfo,
print_cpuinfo, /* 于打印 CPU 信息 */
embedded_dtb_select,
show_board_info, //打印板子信息
INIT_FUNC_WATCHDOG_INIT //初始化看门狗
misc_init_f,
INIT_FUNC_WATCHDOG_RESET //复位看门狗
init_func_i2c, //初始化 I2C
init_func_vid,
announce_dram_init,
dram_init, /* configure available RAM banks */
post_init_f,
INIT_FUNC_WATCHDOG_RESET
testdram,
INIT_FUNC_WATCHDOG_RESET
init_post,
INIT_FUNC_WATCHDOG_RESET
setup_dest_addr,
fix_fdt,
reserve_pram,
reserve_round_4k, //gd->relocaddr 做 4KB 对 齐
arch_reserve_mmu, //留出 MMU 的 TLB 表的位置
reserve_video,
reserve_trace,
reserve_uboot, //留出重定位后的 uboot 所占用的内存区域
reserve_malloc, //留出 malloc 区域
reserve_board, //留出板子 bd 所占的内存区
reserve_global_data, //保留出 gd_t 的内存区域
reserve_fdt, //留出设备树相关的内存区域
reserve_bootstage,
reserve_bloblist,
reserve_arch, //设置机器 ID
reserve_stacks, //留出栈空间
dram_init_banksize,
show_dram_config, //显示 DRAM 的配置
INIT_FUNC_WATCHDOG_RESET
setup_bdinfo,
display_new_sp, //显示新的 sp 位置
INIT_FUNC_WATCHDOG_RESET
reloc_fdt,
reloc_bootstage,
reloc_bloblist,
setup_reloc, //设置 gd 的其他一些成员变量
copy_uboot_to_ram,
do_elf_reloc_fixups,
clear_bss,
cyclic_unregister_all,
jump_to_copy,
NULL,
};
重定位目的地址:setup_dest_addr
3.4 重定位向量表
函数 relocate_vectors 用于重定位向量表,此函数定义在文件 relocate.S
WEAK(relocate_vectors)
#ifdef CONFIG_CPU_V7M
/*
* On ARMv7-M we only have to write the new vector address
* to VTOR register.
*/
ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
ldr r1, =V7M_SCB_BASE
str r0, [r1, V7M_SCB_VTOR]
#else
#ifdef CONFIG_HAS_VBAR
/*
* If the ARM processor has the security extensions,
* use VBAR to relocate the exception vectors.
*/
ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
mcr p15, 0, r0, c12, c0, 0 /* Set VBAR */
#else
/*
* Copy the relocated exception vectors to the
* correct address
* CP15 c1 V bit gives us the location of the vectors:
* 0x00000000 or 0xFFFF0000.
*/
ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */重定位后 uboot 的首地址,向量表肯定是从这个地址开始存放的
mrc p15, 0, r2, c1, c0, 0 /* V bit (bit[13]) in CP15 c1 */
ands r2, r2, #(1 << 13)
ldreq r1, =0x00000000 /* If V=0 */
ldrne r1, =0xFFFF0000 /* If V=1 */
ldmia r0!, {r2-r8,r10}
stmia r1!, {r2-r8,r10}
ldmia r0!, {r2-r8,r10}
stmia r1!, {r2-r8,r10}
#endif
#endif
ret lr
ENDPROC(relocate_vectors)
3.5 board_init_r
了 board_init_f 函数,在此函数里面会调用一系列的函数来初始化一些外设和 gd 的成员变量。但是 board_init_f 并没有初始化所有的外设,还需要做一些后续工作,这些后续工作就是由函数 board_init_r 来完成的,board_init_r 函数定义在文件common/board_r.c中,代码如下:
void board_init_r(gd_t *new_gd, ulong dest_addr)
{
gd->flags &= ~(GD_FLG_SERIAL_READY | GD_FLG_LOG_READY);
if (CONFIG_IS_ENABLED(X86_64) && !IS_ENABLED(CONFIG_EFI_APP))
arch_setup_gd(new_gd);
#if !defined(CONFIG_X86) && !defined(CONFIG_ARM) && !defined(CONFIG_ARM64)
gd = new_gd;
#endif
gd->flags &= ~GD_FLG_LOG_READY;
if (IS_ENABLED(CONFIG_NEEDS_MANUAL_RELOC)) {
for (int i = 0; i < ARRAY_SIZE(init_sequence_r); i++)
MANUAL_RELOC(init_sequence_r[i]);
}
if (initcall_run_list(init_sequence_r)) //initcall_run_list 函数来执行初始化序列 init_sequence_r
hang();
/* NOTREACHED - run_main_loop() does not return */
hang();
}
init_sequence_r 是一个函数集合,init_sequence_r 也定义在文件common/board_r.c 中
static init_fnc_t init_sequence_r[] = {
initr_trace, //初始化和调试跟踪有关的内容
initr_reloc, //设置 gd->flags,标记重定位完成
event_init,
initr_caches, //初始化 cache,使能 cache
initr_reloc_global_data, //初始化重定位后 gd 的一些成员变量
- initr_console_record 函数,初始化控制台相关的内容
- board_init 函数,板级初始化
- initr_serial 函数,初始化串口
- power_init_board 函数,初始化电源芯片
- initr_nand 函数,初始化 NAND
- initr_mmc 函数,初始化 EMMC
- initr_env 函数,初始化环境变量
- initr_secondary_cpu 函数,初始化其他 CPU 核
- stdio_add_devices 函数,各种输入输出设备的初始化
- initr_jumptable 函数,初始化跳转表
- interrupt_init 函数,初始化中断
- initr_enable_interrupts 函数,使能中断
- initr_ethaddr 函数,初始化网络地址
- board_late_init 函数,板子后续初始化
- initr_net 函 数 , 初 始 化 网 络 设 备
参考:
后记:
这篇写的有点云里雾里,留作有问题的时候来查询的内容吧,对于汇编语言可以借助AI:例如文心一言去帮我们解释代码,也不必要自己去系统的学一遍ARM汇编,要想继续深入研究还是需要去看ARM手册,也可以去看手册里面的操作来代码里面看哪里用了,怎么用的。