ARM8 U-boot启动源码分析(学习笔记) 电脑版发表于:2024/7/19 22:38 ![](https://img.tnblog.net/arcimg/hb/6930d1439f4b43e785a433685b813262.png) >#ARM8 U-boot启动源码分析(学习笔记) [TOC] 什么是U-Boot? ------------ tn2>U-Boot是嵌入式系统中首先执行的程序之一。 也是开源引导程序。 安装Jetson BSP ------------ tn2>下载Jetson BSP包:https://developer.nvidia.com/embedded/jetson-linux-archive 我这里下的是:https://developer.nvidia.com/embedded/linux-tegra-r3275 ![](https://img.tnblog.net/arcimg/hb/76bd2c3a251943ab899ea0deb356a158.png) tn2>选择驱动程序包(BSP)来源进行下载源码。 ```bash scp public_sources.tbz2 yhai@192.168.153.129:/home/yhai/armv8/armboot tar -xvf public_sources.tbz2 cd Linux_for_Tegra/source/public tar -xvf u-boot_src.tbz2 //解压u-boot cd u-boot ctags -R //进入代码根目录,生成符号索引 cscope -Rbq ``` ![](https://img.tnblog.net/arcimg/hb/a92a18633a13417c9ee79f142a8f6b73.png) ### 编译qemu版的u-boot ```bash cd u-boot ls configs |grep qemu # 查看可以编译的配置 make qemu_arm64_defconfig # 选择qemu支持的arm64配置 make #编译有生成u-boot.bin表示成功 ``` tn>遇到了下面的uboot问题,请安装bison解决。 ![](https://img.tnblog.net/arcimg/hb/146ea99337f1450fb6e4af29d698c618.png) ```bash sudo apt-get install bison -y ``` tn>遇到了缺少flex的问题,通过如下命令安装后总算是成功了。 ![](https://img.tnblog.net/arcimg/hb/c1b3072d56da43e7936e21ebe218b3a9.png) tn>在make的时候有遇到这个问题。 ![](https://img.tnblog.net/arcimg/hb/9cb477f458ce40428cafaf535b1793d1.png) tn>如果缺少openssl包,请到官网找到相应的包,然后用`dpkg`进行安装。 官网链接:http://archive.ubuntu.com/ubuntu/pool/main/o/openssl/ ```bash sudo apt install gcc-aarch64-linux-gnu export CROSS_COMPILE=aarch64-linux-gnu- #在编译 U-Boot 时,你需要设置交叉编译器环境变量。 # 重新编译 make distclean make qemu_arm64_defconfig make ``` ![](https://img.tnblog.net/arcimg/hb/329980bcec2a48afb1c4b37ed0699b66.png) 源码入口 ------------ tn2>对于任何程序,入口函数是在链接时决定的,所以u-boot的入口是由链接脚本决定的。 u-boot下armv8默认的链接文件是`u-boot.lds`。 当然我们也可以通过`CONFIG_SYS_LDSCRIPT`在`Makefile`进行指定路径。 入口地址也是由连接器决定的,在配置文件中可以由`CONFIG_SYS_TEXT_BASE`指定,这个会在编译时在ld连接器的选项-Ttext中。 ![](https://img.tnblog.net/arcimg/hb/fd46c3f733d94f74aea3b6cf3405fd0d.png) ![](https://img.tnblog.net/arcimg/hb/f2a723806c164edba3b8855d433d46f4.png) | 配置 | 描述 | | ------------ | ------------ | | `OUTPUT_FORMAT` | 这行代码指定了链接器生成的输出文件的格式。 | | `OUTPUT_ARCH` | 这行代码指定了目标架构为aarch64。 | | `ENTRY(_start)` | 这行代码指定了程序的入口点函数为`_start`。 | | `*(.__image_copy_start)` |将名为__image_copy_start的符号插入到.text段的开头。| | `arch/arm/cpu/armv8/start.o (.text*)` |将start.o文件中所有以.text开头的段放入.text段。| tn2>接着我们来看一下`start.S`文件。 ![](https://img.tnblog.net/arcimg/hb/2e039e6ca67d4de780cd57fcb4f30681.png) tn2>`.global`声明`_start`为全局符号,`_start`就会被连接器链接到,也就是链接脚本中的入口地址。 这段代码根据预编译宏定义选择性地包含不同的头文件,或者跳转到`reset`标签。 如果定义了`CONFIG_LINUX_KERNEL_IMAGE_HEADER`,则包含`<asm/boot0-linux-kernel-header.h>`文件。 如果定义了`CONFIG_ENABLE_ARM_SOC_BOOT0_HOOK`,则包含`<asm/arch/boot0.h>`文件。 否则,跳转到reset标签。 tn>那么这些定义是在哪儿定义的呢?是在根目录中的`.config`下进行定义的 ![](https://img.tnblog.net/arcimg/hb/92ecfe2c51f947bf9ed6969be0d7486d.png) tn2>在`.config`中两个都未定义,自然就执行`b reset`进行重置的方法了。 下面是执行重置方法的流程图。 ```mermaid graph LR; A[reset] --> B[save_boot_params]; B --> C[save_boot_params_ret]; ``` ![](https://img.tnblog.net/arcimg/hb/acea27d29b79405fa878d1c1d87d324f.png) tn2>其中有设置异常的方法,那么我们应该如何跳转过去呢?可以使用`vim`。 ![](https://img.tnblog.net/arcimg/hb/c228c15e0ca343fca9651f1e4e4ee1fc.png) tn2>通过vim在根目录下打开文件,找到我们的`Start.S`文件中的vectors函数,然后通过按一下`Ctrl+]`跳转到对应的函数位置。 ![](https://img.tnblog.net/arcimg/hb/230342f9b24b44359afff7a89251b367.png) tn>如果发现跳转不过去,请在根目录下执行`ctags -R .`命令,生成标签文件方便跳转。 tn2>然后我们可以通过`ctrl+o`跳转回来。 ![](https://img.tnblog.net/arcimg/hb/64e4e191ccf34ba89e27d1f60948523e.png) tn2>接着又是对异常中断的处理。 ![](https://img.tnblog.net/arcimg/hb/67daf6a702f84668a24e306dfcf4c99c.png) tn2>我们来看一下`switch_el`宏函数。 ![](https://img.tnblog.net/arcimg/hb/3596c6b834684e229fe7798010dde05b.png) tn2>根据获取`CurrentEL`当前异常状态进行获取,进行el3,el2,el1不同的异常进行处理。 为什么是`0xc`,`0x8`,`0x4`。这是由于armv8对应的当前异常处理的值 ![](https://img.tnblog.net/arcimg/hb/83f0278d31ca40aba4a576f6171eec79.png) tn2>接着在`3:`中有调用了`set_vbar`宏函数,它将在`start.S`文件中定义,其实就是获取异常处理特殊寄存器的处理附上处理方式。 ![](https://img.tnblog.net/arcimg/hb/d0421fe666c04de4871002acde9ce8b1.png) tn2>定义好异常处理后,接着调用了`apply_core_errata`这个函数。 可以通过按`/`和`Enter`键来查找。 ![](https://img.tnblog.net/arcimg/hb/a2e15872356747b7821c99874cc29d9e.png) tn2>这个函数对CPU做了一些处理。<br/> `branch_if_a53_core x0, apply_a53_core_errata`:检查当前是否运行在 Cortex-A53 核心上。如果是,则跳转到 `apply_a53_core_errata` 标签,执行针对 Cortex-A53 的特定错误修复操作。<br/> `branch_if_a57_core x0, apply_a57_core_errata`:检查当前是否运行在 Cortex-A57 核心上。如果是,则跳转到 `apply_a57_core_errata` 标签,执行针对 Cortex-A57 的特定错误修复操作。 ![](https://img.tnblog.net/arcimg/hb/42181b468c624f1c8035dd2974d336f7.png) tn2>然后调用了`bl lowlevel_init`进行低级别初始化。 下面是它的定义: ![](https://img.tnblog.net/arcimg/hb/83f5ca9add1c41df93d35f65f24e7eba.png) tn2>接着就是处理 ARM 处理器多核系统中的从属 CPU(即不是主 CPU 的其他处理器核心)的启动逻辑。 ![](https://img.tnblog.net/arcimg/hb/b44b5141b7bd451eab55dab960a6618f.png) tn2>核心的cpu将直接调用`_main`方法。 如果找不到`main`函数,我们还可以从`u-boot.map`中找到对应的函数。 ![](https://img.tnblog.net/arcimg/hb/e3082eba595540fa988a92fa43ab97d6.png) ![](https://img.tnblog.net/arcimg/hb/ef6fe124cd4c496caac03e7c93e432c9.png) tn2>然后通过对应的文件夹找到相应的`_main`函数。 ![](https://img.tnblog.net/arcimg/hb/44deffa49ebf4be0a42f37cdb634524a.png) tn2>`crt0`是运行C代码之前的初始化代码。 `crt0_64.S`文件中有非常详细的注释。 ![](https://img.tnblog.net/arcimg/hb/61b026405d5e43328ebb8ca26a3537ac.png) ### 复位系统配置函数 tn2>主要也是在`start.S`中的`reset_sctrl`函数 ![](https://img.tnblog.net/arcimg/hb/c7725df557434c5cac9253521ff86b0c.png) ![](https://img.tnblog.net/arcimg/hb/425b2752232d444ba8125e1a8325600e.png) tn2>我们可以看到首先还是设置的异常级别的处理。 在每个异常级别里面都进行调用了`b 0f`为了,调用0函数。里面主要是为了清除指定位。 ```bash ldr x1, =0xffffffffa //将值0xffffffffa加载到寄存器x1中。 and x0, x0, x1 //对x0寄存器中的值进行按位与操作,清除后四位。 ``` tn2>接着x1中保留当前的异常处理状态,然后再次调用`switch_el`,将EL3,EL2,EL1,分别对应6,5,4。 其中都调用了`7`。 ```bash dsb sy // 数据同步屏障指令,确保所有的内存访问完成。 isb // 指令同步屏障指令,确保所有之前的指令执行完成。 b __asm_invalidate_tlb_all // 跳转到__asm_invalidate_tlb_all函数,可能是用于失效TLB(翻译后备缓冲)。 ret //返回。 ``` ### 低级初始化 tn2>首先保存了返回的地址到`x29`寄存器。 ![](https://img.tnblog.net/arcimg/hb/75efb41011fd4a13b8751833d4c17393.png) ```bash WEAK(lowlevel_init) mov x29, lr /* 保存链接寄存器 (LR) 到 x29 中 */ #if defined(CONFIG_GICV2) || defined(CONFIG_GICV3) branch_if_slave x0, 1f /* 如果处理器不是主处理器,则跳转到标签 1 */ ldr x0, =GICD_BASE /* 将中断分配器接口的基地址加载到 x0 寄存器中 */ bl gic_init_secure /* 调用函数初始化 GIC 的安全部分 中断控制器的初始化 */ 1: #if defined(CONFIG_GICV3) ldr x0, =GICR_BASE /* 将重新分配器接口的基地址加载到 x0 寄存器中 */ bl gic_init_secure_percpu /* 调用函数初始化 GIC 的每个 CPU 的安全部分 */ #elif defined(CONFIG_GICV2) ldr x0, =GICD_BASE /* 将中断分配器接口的基地址加载到 x0 寄存器中 */ ldr x1, =GICC_BASE /* 将 CPU 接口的基地址加载到 x1 寄存器中 */ bl gic_init_secure_percpu /* 调用函数初始化 GIC 的每个 CPU 的安全部分 */ #endif #endif ``` tn2>这里`GICD_BASE`基地址是根据不同的板子进行定义的。 接着`bl gic_init_secure`执行的是中断处理(GIC控制器)的初始化。 tn>GIC 的主要功能包括: 中断管理:接收和管理来自各种外设的中断信号。 中断分发:根据中断的优先级和配置,将中断信号分发给适当的处理器核心。 中断处理:提供中断处理的相关寄存器和接口,支持中断的使能、屏蔽和优先级管理。 ```bash .macro branch_if_slave, xreg, slave_label #ifdef CONFIG_ARMV8_MULTIENTRY /* 注意:在多簇机器上,这段代码可能会有问题 */ mrs \xreg, mpidr_el1 /* 读取处理器ID寄存器的值到 \xreg */ tst \xreg, #0xff /* 检查最低8位是否为0(处理器的第一级亲和度) */ b.ne \slave_label /* 如果不是0,就跳转到 slave_label(从CPU) 标签 */ lsr \xreg, \xreg, #8 /* 右移8位,准备检查下一级 */ tst \xreg, #0xff /* 检查第二级亲和度 */ b.ne \slave_label /* 如果不是0,就跳转到 slave_label(从CPU) 标签 */ lsr \xreg, \xreg, #8 /* 再右移8位,准备检查下一级 */ tst \xreg, #0xff /* 检查第三级亲和度 */ b.ne \slave_label /* 如果不是0,就跳转到 slave_label(从CPU) 标签 */ lsr \xreg, \xreg, #16 /* 再右移16位,准备检查最后一级 */ tst \xreg, #0xff /* 检查第四级亲和度 */ b.ne \slave_label /* 如果不是0,就跳转到 slave_label(从CPU) 标签 */ #endif .endm ``` tn2>再往下其实就是不同的级别进行不同的执行和操作。 ### 异常向量表 tn2>主要的文件在`arch/arm/cpu/armv8/exceptions.S`文件下。 文件的注释讲得很好,我翻译一下: * AArch64 异常向量: * 我们有四种类型的异常: * - 同步:陷阱、数据中止、未定义指令,... * - IRQ:第 1 组(正常)中断 * - FIQ:第 0 组或安全中断 * - SError:致命系统错误 * 以上四个条目针对不同的上下文都有: * - 使用 SP_EL0 堆栈指针时,来自同一异常级别 * - 使用 SP_ELx 堆栈指针时,来自同一异常级别 * - 当这是 AArch64 时,来自较低异常级别 * - 当这是 AArch32 时,来自较低异常级别 * 这 16 个条目中的每一个都有 32 条指令的空间,每个条目必须 128 字节对齐,整个表必须 2K 对齐。 * 32 条指令不足以保存和恢复所有寄存器并分支到实际处理程序,因此我们将其拆分: * 每个条目保存 LR,分支到保存例程,然后到实际处理程序,然后到恢复例程。保存和恢复例程分别分成两半并塞入条目之间未使用的间隙中。 * 此外,由于我们不在较低的异常级别运行任何内容,我们仅提供来自同一 EL 的异常的前 8 个条目。 ![](https://img.tnblog.net/arcimg/hb/aa2db0c8b972495db8c9a57a29fafde3.png) tn2>首先就是`.align 11`进行2^11=2048 整个异常向量表进行2k对齐 --> 通过对齐,实现向量空间的预留 16个异常每个异常32条指令 16*32*4=2048 接着`.globl`进行全局方法名的声明。 第一个就是同步异常,然后`stp x29, x30, [sp, #-16]!`这条指令表示:将寄存器 `x29` 和 `x30` 的值存储到栈上,同时将栈指针 `sp` 向下移动 `16` 个字节(因为每个寄存器占用 8 个字节的存储空间,总共需要 16 个字节),以预留存储空间。 这种操作通常用于函数调用的开头,用来保存返回地址(x30)和帧指针(x29),以便在函数返回时恢复这些值。这里每当偏移16个字节就会有一个这样的操作对应的是寄存器中的每一个异常处理。 如下图所示: ![](https://img.tnblog.net/arcimg/hb/741c50e6467844afa551dce7c8d07382.png) tn2>接着执行`bl _exception_entry`保存现场所有的值到栈中。 ![](https://img.tnblog.net/arcimg/hb/b66bd4ae28db4edb93c5afdfaee03a0c.png) tn2>当然除了保存现场长常规寄存器,还保存了特殊寄存器,也就是调用了`b _save_el_regs`方法。 其中主要争对ELR和ESR进行保存。(但我其实看到它好像只保存了ELR的信息没有保存SER的) 代码如下: ![](https://img.tnblog.net/arcimg/hb/02e034382c2546f1bd05f49d3077d9ed.png) ![](https://img.tnblog.net/arcimg/hb/36363bdb24fe42598f075ff56eb1e657.png) tn>ELR:获取导致异常的指令的地址。 ESR:包括有关异常信息的原因。 tn2>接着返回到上面,执行`bl do_bad_sync`命令。 在`/Linux_for_Tegra/source/public/u-boot/arch/arm/lib/interrupts_64.c`文件中找到同步处理的方法。 ![](https://img.tnblog.net/arcimg/hb/2bfb8796344c47d2b4c48b051c4395fb.png) tn2>最后调用`b exception_exit`指令进行恢复现场里面就是一些出栈的操作。 ![](https://img.tnblog.net/arcimg/hb/d02d04e02b5b40789806b3ca1e038987.png) ![](https://img.tnblog.net/arcimg/hb/3d942a5ba155407cb7207c111bca2f2d.png) tn2>`eret`返回当初应用异常的位置,这样一系列的操作就完成了下面这幅图的流程。 ![](https://img.tnblog.net/arcimg/hb/8a145e1fec53475688c4c46cea13a968.png) tn2>中断过程的流程同样有些相似。 在刚刚下面就是处理中断异常的代码。 ![](https://img.tnblog.net/arcimg/hb/385fb09d93014d58aa640ed78501c798.png) ![](https://img.tnblog.net/arcimg/hb/dd382daf247740c9895dd7fa71937cb1.png) ### 中断控制器初始化 ![](https://img.tnblog.net/arcimg/hb/00cd88c376374ce0bda1a5ba2e8a3d36.png) tn2>`bl gic_init_secure`命令执行的是中断处理(GIC控制器)的初始化操作,这段代码我们在低级初始化的时候提到过。 在这之前执行了获取GIC基地址到w`x0`寄存器中,然后由`gic_init_secure`操作。 ![](https://img.tnblog.net/arcimg/hb/fe97242ce4dd4edcb27a8f9036972983.png) tn2>任何硬件或软件发送中断型号到管脚中,然后发送到CPU进行处理。 ![](https://img.tnblog.net/arcimg/hb/b0effb2de49d4c75b5d9085a99f2beb7.png) tn2>像按键触发到GIC中断控制器后,将再次发到多核中进行处理。 关于它定义的基地址`50041000`可以查看芯片手册。 ### C语言执行环境初始化 tn2>`crt0_64.S`文件主要是初始化c语言的代码。 ![](https://img.tnblog.net/arcimg/hb/2cf0d4104c004c61a24ccb6809b163d4.png) tn2>1.设置栈堆。如果当前的编译是`SPL`(由`CONFIG_SPL_BUILD`定义),可单独定义堆栈基址(`CONFIG_SPL_STACK`),否则,通过`CONFIG_SYS_INIT_SP_ADDR`定义栈堆。<br/> 2.调用`board_init_f_alloc_reserve`接口,从栈堆开始的地方,为全局数据(gd`global data`)结构分配空间。 也就是预留一些数据空间。 <br/> 接着调用`board_init_f()`函数,完成一些前期的初始化工作 ![](https://img.tnblog.net/arcimg/hb/ccbcfda187b94962ba632bed64de2ba5.png) tn2>`board_init_f()`函数的一些源码,例如: 点亮一个Debug用的LED灯,表示u-boot已经激活。 初始化DRAM、DDR等system范围的RAM。 启用指令缓存(Instruction Cache)。 3.如果当前是SPL(由`CONFIG_SPL_BUILD`控制),则`_main()`函数结束,直接返回。如果是正常的u-boot,则继续执行后续的动作。 4.根据`board_init_f()`函数指定的参数,执行`u-boot`的relocation操作。 5.清理BBS段。 6.调用`board_init_r()`函数,执行后续的初始化炒作。 #### board_init_f()函数 tn2>board_init_f()函数主要是更具配置全局信息结构体gd进行初始化,主要工作内容如下: 1.初始化一系列外设,比如串口、定时器,获取打印一些消息等。 2.初始化gd(global data)的各个成员变量,u-boot会将自己重定位到DRAM后面的地址区域,也就是将自己复制到DRAM(DRAM [Dynamic Random-Access Memory,动态随机存取存储器]是一种广泛使用的存储器类型,用于计算机和其他电子设备中的主内存。)后面的的内存区域中。 这么做的目的是给Linux腾出空间,防止Linuxkernel覆盖u-boot,将DRAM当前的区域完整的空出来。 在复制之前肯定要给u-boot各个部分分配好内存位置和大小,比如gd应该存放到哪个位置,malloc内存池应该放到哪个位置等。 这些信息都保存在gd的成员变量中,因此要对gd的这些成员变量做初始化。 #### relocate_code()函数 tn2>接着我们回到`_main`方法。 这段代码主要是更新gd结构体。 ![](https://img.tnblog.net/arcimg/hb/be9a14bf8ec64acda79aa3682e24da24.png) ``` #if !defined(CONFIG_SPL_BUILD) /* * Set up intermediate environment (new sp and gd) and call * relocate_code(addr_moni). Trick here is that we'll return * 'here' but relocated. */ ldr x0, [x18, #GD_START_ADDR_SP] /* 从gd结构中加载start_addr_sp到x0寄存器 */ bic sp, x0, #0xf /* 将sp寄存器设置为x0的值,并且16字节对齐,满足ABI规范 */ ldr x18, [x18, #GD_NEW_GD] /* 从gd结构中加载new_gd到x18寄存器 */ adr lr, relocation_return /* 将重定位返回地址加载到链接寄存器lr */ #if CONFIG_POSITION_INDEPENDENT /* Add in link-vs-runtime offset */ adr x0, _start /* 获取当前_start的运行时地址到x0寄存器 */ ldr x9, _TEXT_BASE /* 获取_start的链接时地址到x9寄存器 */ sub x9, x9, x0 /* 计算运行时地址与链接时地址的偏移量并存储在x9寄存器 */ add lr, lr, x9 /* 将这个偏移量加到lr寄存器中,得到新的返回地址 */ #endif /* Add in link-vs-relocation offset */ ldr x9, [x18, #GD_RELOC_OFF] /* 从gd结构中加载重定位偏移量到x9寄存器 */ add lr, lr, x9 /* 将这个偏移量加到lr寄存器中,得到新的返回地址 */ ldr x0, [x18, #GD_RELOCADDR] /* 从gd结构中加载重定位地址到x0寄存器 */ b relocate_code /* 跳转到重定位代码 */ relocation_return: /* 重定位完成后的返回标签 */ #endif ``` tn2>`b relocate_code`这段代码主要负责重定位U-Boot的监控代码。 这段代码主要负责重定位U-Boot的监控代码。 在嵌入式系统中,U-Boot通常在启动时将其自身从非易失性存储(如闪存)复制到易失性存储(如RAM),然后继续执行。 ```bash #include <asm-offsets.h> #include <config.h> #include <elf.h> #include <linux/linkage.h> #include <asm/macro.h> /* * void relocate_code(addr_moni) * * This function relocates the monitor code. * x0 holds the destination address. */ ENTRY(relocate_code) stp x29, x30, [sp, #-32]! /* 创建一个栈帧,保存x29和x30寄存器 */ mov x29, sp str x0, [sp, #16] /* * 将u-boot从闪存复制到RAM */ adrp x1, __image_copy_start /* x1 <- 地址的高20位 */ add x1, x1, :lo12:__image_copy_start/* x1 <- 地址的低12位 */ subs x9, x0, x1 /* x9 <- 运行时地址与复制地址的偏移量 */ b.eq relocate_done /* 如果已经在正确位置,跳过重定位 */ /* * 不能在此处使用ldr x1, __image_copy_start,因为如果代码已经 * 运行在不同于链接地址的位置,该指令会加载重定位后的值。 * 为了正确应用重定位,我们需要知道链接时的值。 * * 链接时的&__image_copy_start,我们知道它在 * CONFIG_SYS_TEXT_BASE,该值存储在_TEXT_BASE, * 不是符号引用,所以不会被重定位。 */ ldr x1, _TEXT_BASE /* x1 <- 链接时的&__image_copy_start */ subs x9, x0, x1 /* x9 <- 链接地址与复制地址的偏移量 */ adrp x1, __image_copy_start /* x1 <- 地址的高20位 */ add x1, x1, :lo12:__image_copy_start/* x1 <- 地址的低12位 */ adrp x2, __image_copy_end /* x2 <- 地址的高20位 */ add x2, x2, :lo12:__image_copy_end /* x2 <- 地址的低12位 */ copy_loop: ldp x10, x11, [x1], #16 /* 从源地址[x1]复制 */ stp x10, x11, [x0], #16 /* 到目标地址[x0] */ cmp x1, x2 /* 直到源地址的结束地址[x2] */ b.lo copy_loop str x0, [sp, #24] /* * 修正.rela.dyn重定位 */ adrp x2, __rel_dyn_start /* x2 <- 地址的高20位 */ add x2, x2, :lo12:__rel_dyn_start /* x2 <- 地址的低12位 */ adrp x3, __rel_dyn_end /* x3 <- 地址的高20位 */ add x3, x3, :lo12:__rel_dyn_end /* x3 <- 地址的低12位 */ fixloop: ldp x0, x1, [x2], #16 /* (x0,x1) <- (源位置, 修正值) */ ldr x4, [x2], #8 /* x4 <- 加数 */ and x1, x1, #0xffffffff cmp x1, #R_AARCH64_RELATIVE bne fixnext /* 进行相对修正:将加数加上偏移量存储在目标位置 */ ``` tn>简单来讲:这段代码的作用是将 U-Boot 程序从闪存(或其他非易失性存储器)复制到 RAM(随机存取存储器)中。 这样做的原因是 RAM 的速度比闪存快,程序运行起来会更高效。 #### board_init_r()函数 tn2>从`relocate_code()`函数回到`_main()`函数中,接下来是`main()`函数最后一段代码,如图11-17所示。 ![](https://img.tnblog.net/arcimg/hb/d6e851f595064df19b04648e9f0eba61.png) tn2>首先跳转到`c_runtime_cpu_setup`中,如果`icache`为启用,则`icache`无效,保证从sdram中更新指令到cache中。接着更新异常向量表首地址,因为代码被重新定位,所以异常向量表也被重新定位。 接着清空BBS段。 ```bash bl c_runtime_cpu_setup /* 调用旧的例行程序,进行CPU的初始化设置 */ #endif /* !CONFIG_SPL_BUILD */ #if !defined(CONFIG_SPL_BUILD) || CONFIG_IS_ENABLED(FRAMEWORK) #if defined(CONFIG_SPL_BUILD) bl spl_relocate_stack_gd /*该函数负责在 SPL 构建(Secondary Program Loader,二级引导程序)期间进行栈和全局数据结构 gd 的重定位。 可能返回NULL,调用spl_relocate_stack_gd函数 */ /* set up gd here, outside any C code, if new stack is returned */ cmp x0, #0 csel x18, x0, x18, ne /* * Perform 'sp = (x0 != NULL) ? x0 : sp' while working * around the constraint that conditional moves can not * have 'sp' as an operand */ mov x1, sp cmp x0, #0 csel x0, x0, x1, ne mov sp, x0 #endif ``` ![](https://img.tnblog.net/arcimg/hb/4ee68d8e8bce44898790eedbcd0f26bc.png) tn2>将`x0`赋值gd指针,`x1`赋值relocaddr变量sp(栈指针),然后进入`board_init_r()`函数。 ![](https://img.tnblog.net/arcimg/hb/39904f6905e740448eb17e8680124c20.png) tn2>`board_init_r()`函数是需要实现的版级支持函数,做开发板的基本初始化. 该函数是实现开发板所有功能的初始化,包括CPU、内存、串口、电源、环境变量、中断、网络等。 <br/> 比如串口初始化,`init_sequence_r()`函数中有`initr_serial`命令调用`serial_initialize()`函数源码实现在`drivers/serial/serial.c`文件中。 ![](https://img.tnblog.net/arcimg/hb/60245ac420dc40dc90451e662579181e.png) ![](https://img.tnblog.net/arcimg/hb/9807656924f642a59adf870e62e3cd5b.png) ![](https://img.tnblog.net/arcimg/hb/8f3deb055dec44679fe97e6fd2cfbd5e.png) tn2>里面有很多平台的串口驱动。 ```bash void serial_initialize(void) { // 初始化 Atmel 的串行端口 atmel_serial_initialize(); // 初始化 ColdFire 的串行端口 mcf_serial_initialize(); // 初始化 Freescale MPC85xx 的串行端口 mpc85xx_serial_initialize(); // 初始化 Freescale i.MX 的串行端口 mxc_serial_initialize(); // 初始化 NS16550 兼容的串行端口 ns16550_serial_initialize(); // 初始化 ARM PL010 和 PL011 串行端口 pl01x_serial_initialize(); // 初始化 PXA 的串行端口 pxa_serial_initialize(); // 初始化 SuperH 的串行端口 sh_serial_initialize(); // 初始化 MediaTek 的串行端口 mtk_serial_initialize(); // 分配默认串行控制台 serial_assign(default_serial_console()->name); } ``` tn2>这些函数将所有需要的串口(用结构体struct serial_device函数表示,其中实现了基本的收发配置)调用`serial_register`函数注册。 ![](https://img.tnblog.net/arcimg/hb/2ec2618edaa44c60a0c8f020f9df99d7.png) tn2>将`serial_dev()`函数加到全局链表中`serial_devices`中。可以想象,如果有4个串口则在串口驱动中分别定义4个串口设备,并实现对应的收发配置,然后调用`serial_register()`函数注册4个串口。<br/> 串口注册后,代码回到`serial_initialize`中继续执行`serial_assign(default_serial_console()->name);`,串口驱动给出一个默认调试串口,`serial_assign()`函数的部分内容下面所示: ```c /** * serial_assign() - 选择串行输出设备 * @name: 需要作为默认输出的串行驱动名称 * * 这个函数通过选择哪个串行设备作为默认输出来配置串行输出的多路复用。 * 如果 STDIO "serial" 设备被选择作为标准输入/输出/错误输出, * 那么之前由这个函数配置的串行设备将用于特定的操作。 * * 成功时返回 0,错误时返回负值。 */ int serial_assign(const char *name) { struct serial_device *s; // 遍历所有已注册的串行设备 for (s = serial_devices; s; s = s->next) { // 比较当前设备的名称与传入的名称 if (strcmp(s->name, name)) continue; // 如果名称不匹配,则继续检查下一个设备 // 如果名称匹配,则将当前设备设置为默认串行设备 serial_current = s; return 0; // 成功返回 0 } // 如果没有找到匹配的设备,返回错误代码 -EINVAL return -EINVAL; } ``` tn2>首先`serial_assign()`函数就是从`serial_devices`函数扎到指定的默认调试串口,其次条件就是串口`default_Serial_Console`变量中默认的串口设备名,最后`serial_current()`函数就是当前的默认串口。 `serial_initialize`函数的工作是将`serial`驱动中所有的串口注册到`serial_devices()`函数链表中,然后找到指定的默认串口。 <br/> 回到`board_r.c`文件中的`init_sequence_r`函数我们往下看可以看到初始化电源部分、nand初始化、API()函数初始化、控制台初始化、中断使能等,最后到执行`run_main_loop()`函数。 ![](https://img.tnblog.net/arcimg/hb/80cc242df9d94e2da4baffab78e3d000.png) ![](https://img.tnblog.net/arcimg/hb/627e65066b6e45aba7e912c12247df1d.png) tn2>`run_main_loop()`函数调用了`main_loop()`函数,`main_loop()`函数中会有延时函数`bootdelay_process()`。下图就是开机时延时几秒进入内核,然后调用`autoboot_command`环境变量,也就是开机时下任一按键,开始操作控制台。 ![](https://img.tnblog.net/arcimg/hb/59ada21daf8940cdb891e277bf886fee.png) tn2>`autoboot_command()`函数中有`abortboot()`函数调用`` ```c /** * abortboot - 检查是否需要中止启动 * @bootdelay: 启动延迟时间(秒) * * 该函数用于检测是否需要中止启动过程。 * 如果启动延迟时间大于或等于0,则根据配置检查是否需要中止启动。 * * 返回 1 表示中止启动,0 表示继续启动。 */ static int abortboot(int bootdelay) { // 定义一个变量,用于标记是否中止启动,初始值为 0 int abort = 0; // 如果启动延迟时间大于或等于 0 if (bootdelay >= 0) { // 如果启用了带键序列的自动启动中止功能 if (IS_ENABLED(CONFIG_AUTOBOOT_KEYED)) // 调用函数检测键序列是否中止启动,并设置 abort 的值 abort = abortboot_key_sequence(bootdelay); else // 否则,调用函数检测单个按键是否中止启动,并设置 abort 的值 abort = abortboot_single_key(bootdelay); } // 如果启用了静默控制台模式,并且中止启动 if (IS_ENABLED(CONFIG_SILENT_CONSOLE) && abort) // 清除全局数据标志中的静默标志,恢复正常输出 gd->flags &= ~GD_FLG_SILENT; // 返回是否中止启动的标志 return abort; } ``` tn2>这一段主要在中间判断那儿:假设你启动了设备,它会有一段延迟时间(比如几秒钟),在这段时间内,你可以按下某个键来中止启动,让你有机会进入一些调试模式或者其他操作。 代码会检查你是否启用了一个配置选项(CONFIG_AUTOBOOT_KEYED),这个配置决定了你是要按一个特定的键序列(比如连续按下几个特定的键),还是只需要按一个单独的键来中止启动。 如果没有启用键序列方式,那么代码就会调用函数 `abortboot_single_key`。 tn>`abort = abortboot_key_sequence(bootdelay)`:如果启用了键序列方式,这行代码会在启动延迟期间检查你是否按下了正确的键序列。如果你按下了,abort 变量会被设置为 1,表示你想中止启动。 `abort = abortboot_single_key(bootdelay)`:如果没有启用键序列方式,这行代码会在启动延迟期间检查你是否按下了一个特定的键。如果你按下了,abort 变量会被设置为 1,表示你想中止启动。 tn2>在`abortboot_single_key`函数被调用后,会打印`Hit any key to stop autoboot`。 ![](https://img.tnblog.net/arcimg/hb/d9b8de7a86a54f1385b1a9e1d446228f.png) tn2>`main_loop()`函数最后会有`cli_loop()`函数,`cli_loop()`函数调用`cli_simple_loop()`函数,里面有`run_command_repeatable(lastcommand,flag)`函数,如下图所示,最后运行`lastcommand`字符串中的命令启动内核。 ![](https://img.tnblog.net/arcimg/hb/4abfbabb9fa64aba91620b8be50b8937.png) ![](https://img.tnblog.net/arcimg/hb/774d253badce4931ab82009f84a1d9e8.png) ![](https://img.tnblog.net/arcimg/hb/9b979c132269406998e800c7a6b0e814.png) ![](https://img.tnblog.net/arcimg/hb/92bcc45965c04823b90f1828fac05d86.png) ![](https://img.tnblog.net/arcimg/hb/9b419c2fcad2442d9d4065f2aa33afaf.png) 调试源码 ------------ tn2>在u-boot目录下进行编译。 ```bash cd u-boot export CROSS_COMPILE=aarch64-linux-gnu- make qemu_arm64_defconfig make ``` ![](https://img.tnblog.net/arcimg/hb/7b0b933f682745d98d8815527d9b4d73.png) tn2>启动一个具有 4 个 Cortex-A57 CPU 核心和 2048 MB 内存的 ARM64 虚拟机,使用 u-boot.bin 作为引导加载程序,通过命令行界面进行输入输出,启动时暂停并启用 GDB 调试服务器。 ```bash qemu-system-aarch64 -machine virt -cpu cortex-a57 -nographic -smp 4 -m 2048 -bios u-boot.bin -S -s ``` tn2>设置`lanuch.json`调试文件。 ```bash { // 使用 IntelliSense 了解相关属性。 // 悬停以查看现有属性的描述。 // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "(gdb) 启动", "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/u-boot", "args": [], //stopAtEntry 设为true 让它在第一条指令处停下来 "stopAtEntry": true, "cwd": "${fileDirname}", "environment": [], "externalConsole": false, "MIMode": "gdb", // 如果远程登入到linux 服务器上面,路径不用写 /user/bin/gdb-multiarch "miDebuggerPath": "gdb-multiarch", "miDebuggerServerAddress": "localhost:1234", "setupCommands": [ { "description": "为 gdb 启用整齐打印", "text": "-enable-pretty-printing", "ignoreFailures": true } ] } ] } ``` tn2>开始调试。 tn>在调试时如果发现中途没有找到`common.h`文件,打开命令面板:按下 Ctrl+Shift+P,输入并选择“C/C++: Edit Configurations (JSON)”。来解决。 ![](https://img.tnblog.net/arcimg/hb/3909593d30164129863c673b8de09094.png) ![](https://img.tnblog.net/arcimg/hb/ad2017a396cd466888f39cb9b223d67a.png)